Границы признаков в признаках Fn

Возможно, я упускаю что-то простое, но мне трудно убедить компилятор Rust, что этот писатель let mut writer = BufWriter::new(file); можно использовать в привязке к трейту F: Fn(Write) -> Result<()>. Например, ниже работает:

fn write_using_writer<F>(file: &Path, f: F) -> Result<()> 
where
    F: Fn(&mut BufWriter<File>) -> Result<()>,
{
    let file = File::create(file)?;
    let mut writer = BufWriter::new(file);
    let _ = f(&mut writer)?;
    writer.flush()?;
    Ok(())
}

fn write_to_file(data: MySerializableStruct) -> Result<()> {
    let path = Path::new("./output.json");
    
    write_using_writer(path, |writer: &mut BufWriter<File>| 
            serde_json::to_writer(writer, data).map_err(|e| Error::SerdeJson { err: e }))
}

Я хотел обобщить это для некоторого Writer, т. е. я хочу, чтобы потребитель на месте вызова использовал какой-то Writer, не обязательно зная его конкретную реализацию.

Пытаясь успокоить компилятор Rust, я сделал следующее:

fn write_using_writer_a<W, F>(file: &Path, f: F) -> Result<()> 
where
    W: Write,
    F: Fn(W) -> Result<()>,
{
    let _ = ensure_dir(file)?;
    let file = File::create(file)?;
    let mut writer = BufWriter::new(file);
    let _ = f(&mut writer)?;
    writer.flush()?;
    Ok(())
}

Однако компилятор выдает мне следующее сообщение об ошибке:

mismatched types
expected type parameter `W`
found mutable reference `&mut std::io::BufWriter<std::fs::File>`

Что мне не хватает?

Я попробовал заменить F: Fn(W) -> Result<()>, с F: FnMut(W) -> Result<()>, но это не имело никакого значения.

То, что вы пытались написать, говорит: «Вызывающие write_using_writer_a предоставят функцию f, которая принимает тип параметра W по их выбору, но независимо от этого выбора я затем вызову f с &mut BufWriter<File>».

eggyal 20.05.2024 23:36

Как только я прочитал ваш комментарий, я понял, что подпись не имеет смысла. Спасибо! Как отметил @drewtato, мне действительно хотелось, чтобы fn write_using_writer_a() принял Fn, для чего требуется Write; реализация fn write_using_writer_a() должна предоставлять возможность использования «писателя».

madison muir 21.05.2024 10:18

По вашим словам, вы «действительно хотите» использовать предоставленный вызывающим абонентом W: Write, но при этом обеспечивать замыкание с помощью &mut BufWriter<File>. Так что я не понимаю вашей цели.

Chayim Friedman 24.05.2024 02:41

Я переформулировал вопрос для ясности. Я знаю, что первоначальный вопрос вызвал путаницу относительно того, чего я пытался достичь. Надеюсь, теперь это ясно, и второй блок кода был моей попыткой решить проблему (неправильно, как заметили некоторые).

madison muir 24.05.2024 22:00
Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
2
4
115
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Единственное, что можно получить от замены BufWriter в подписи, — это ослабить гарантии вашего API, когда write_using_writer_a является общедоступным. Если вам это не нужно, оставьте BufWriter. Если да, то читайте дальше.

На самом деле вы не хотите, чтобы write_using_writer_a было слишком общим W. Это означало бы, что вызывающая сторона может решить, какой W использовать, но в вашем случае вам нужно принять функцию, которая принимает конкретную запись, но не хотите, чтобы вызывающая сторона знала, какую из них.

В Rust для этого уже есть синтаксис, который выглядит следующим образом:

fn write_using_writer_a<F>(file: &Path, f: F) -> Result<()>
where
    F: for<W: Write> Fn(W) -> Result<()>,

Это называется границей черты более высокого ранга (HRTB), но в настоящее время она работает только на протяжении всей жизни. Вместо этого вы можете создать свой собственный тип, который предоставляет только тот API, который вам нужен — признак Write — и заставить функцию использовать его.

pub struct WriteWrapper<'a>(&'a mut BufWriter<File>);

// implement all methods so that all behavior is preserved
impl Write for WriteWrapper<'_> {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        self.0.write(buf)
    }
    fn flush(&mut self) -> std::io::Result<()> {
        self.0.flush()
    }
    fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize> {
        self.0.write_vectored(bufs)
    }
    fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
        self.0.write_all(buf)
    }
    fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> std::io::Result<()> {
        self.0.write_fmt(fmt)
    }
}

fn write_using_writer_a<F>(file: &Path, f: F) -> Result<()>
where
    F: Fn(WriteWrapper) -> Result<()>,
{
    let file = File::create(file)?;
    let mut writer = BufWriter::new(file);
    f(WriteWrapper(&mut writer))?;
    writer.flush()?;
    Ok(())
}

Теперь вы можете сделать write_using_writer_a общедоступным, и изменение внутреннего BufWriter не будет кардинальным изменением.


Интересный факт: эта функция использует неявный HRTB на протяжении всей жизни WriteWrapper. Это эквивалентно этому:

fn write_using_writer_a<F>(file: &Path, f: F) -> Result<()>
where
    F: for<'a> Fn(WriteWrapper<'a>) -> Result<()>,

Мне очень понравился ваш ответ по нескольким причинам. (1) подумайте, действительно ли мне нужно ослабить гарантии API: делая это явно BufWriter<File>, он намекает пользователю на сайте вызова, как Write реализован. На данный момент у меня есть два типа вызывающих программ: serde_json::to_writer, который принимает Write (в моем примере) и записывает байты в файл, например. writer.write_all(..), оба из которых, очевидно, пишут в файлы, что оправдывает BufWriter в подписи. Однако я (теоретически) думал, как я могу обобщить это.

madison muir 21.05.2024 11:22

и (2) само решение не является волшебным (см. решение @Jmb), но оно явное, поэтому легко увидеть, как оно работает. Раньше я сталкивался с границами признаков более высокого ранга в некоторых API, поэтому я знаю о времени жизни.

madison muir 21.05.2024 11:28

Если можно, у меня возникнет еще один вопрос, чтобы расширить свои знания. В вашей отключенной функции HRTB подпись гласит: «Я обещаю, что ссылка, хранящаяся в BufWriter, будет действительна до тех пор, пока вызывающий абонент использует ее в Fn (замыкание)», не так ли? И причина, по которой в этом случае это не обязательно, связана с правилами исключения Rust?

madison muir 21.05.2024 11:41

@madisonmuir 1) Да, и это самое слабое обещание, которое вы можете дать за всю жизнь. 2) Ага.

drewtato 21.05.2024 21:37
Ответ принят как подходящий

Вы пытаетесь сказать, что вызывающая сторона write_using_writer_a может предоставить замыкание, которое принимает некоторый тип W, который они выберут, но что бы они ни выбрали, вместо этого вы передадите &mut BufWriter<File>. Очевидно, это не может сработать.

Если вы не хотите называть BufWriter<File>, на самом деле вы хотите сказать, что вызывающая сторона write_using_writer_a должна предоставить замыкание, которое принимает некоторый тип W, который вы выбираете, и что им нужно знать только, что W реализует Write. В настоящее время это невозможно в стабильной версии Rust, но можно сделать каждую ночь, используя черту Impl в псевдонимах типов:

#![feature(type_alias_impl_trait)]

use std::fs::File;
use std::path::Path;
use std::io::{ BufWriter, Result, Write };

pub type MyWriter = impl Write;

pub fn write_using_writer_a<F>(file: &Path, f: F) -> Result<()> 
where
    F: Fn(&mut MyWriter) -> Result<()>,
{
    // let _ = ensure_dir(file)?;
    let file = File::create(file)?;
    let mut writer = BufWriter::new(file);
    let _ = f(&mut writer)?;
    writer.flush()?;
    Ok(())
}

Детская площадка

Это очень круто; волшебство, я бы сказал. Безусловно, это самое простое решение, но мне нужно прочитать, как оно на самом деле работает (спасибо за ссылку и решение). Для моего приложения использование ночного компилятора не является проблемой, но для других это может оказаться неэффективным, и в этом случае я бы воспользовался решением, предоставленным @drewtato. Я бы хотел принять оба решения.

madison muir 21.05.2024 10:41

Как уже говорилось в другом ответе, это, вероятно, не то, что вы хотите. Но существуют сценарии, в которых необходим вызов функции с помощью универсальной функции.

Для этого нам нужно создать типаж, который будет выполнять функцию:

trait MyFun {
    fn my_f<W: Write>(&self, w: W) -> Result<(), Error>;
}

Затем мы можем реализовать эту черту для «функций», которые мы отправляем в функцию:

struct ExampleFun;

impl MyFun for ExampleFun {
    fn my_f<W: Write>(&self, w: W) -> Result<(), Error> {
        todo!()
    }
}

Фактическая функция, использующая это:

fn write_using_writer_a<F>(file: &Path, f: F) -> Result<(), Error> 
where
    F: MyFun
{
    let file = File::create(file)?;
    let mut writer = BufWriter::new(file);
    let _ = f.my_f(&mut writer)?;
    writer.flush()?;
    Ok(())
}

И пример main:

fn main() {
    write_using_writer_a(Path::new("/test.txt"), ExampleFun).unwrap();
}

детская площадка

Другие вопросы по теме