С замыканиями в качестве параметров и возвращаемых значений, Fn или FnMut более идиоматически c? - PullRequest
1 голос
/ 15 февраля 2020

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

От эти слайды , я узнал, что для удобства потребителей, вы должны попытаться взять функции за FnOnce и вернуть как Fn, где это возможно. Это дает вызывающей стороне большую свободу, что передавать и что делать с возвращенной функцией.

В моем примере FnOnce невозможно, потому что мне нужно вызывать эту функцию несколько раз. Пытаясь его скомпилировать, я нашел две возможности:

pub enum Parsed<'a, T> {
    Some(T, &'a str),
    None(&'a str),
}

impl<'a, T> Parsed<'a, T> {
    pub fn unwrap(self) -> (T, &'a str) {
        match self {
            Parsed::Some(head, tail) => (head, &tail),
            _ => panic!("Called unwrap on nothing."),
        }
    }

    pub fn is_none(&self) -> bool {
        match self {
            Parsed::None(_) => true,
            _ => false,
        }
    }
}

pub fn achar(character: char) -> impl Fn(&str) -> Parsed<char> {
    move |input|
        match input.chars().next() {
            Some(c) if c == character => Parsed::Some(c, &input[1..]),
            _ => Parsed::None(input),
        }
}

pub fn some_v1<T>(parser: impl Fn(&str) -> Parsed<T>) -> impl Fn(&str) -> Parsed<Vec<T>> {
    move |input| {
        let mut re = Vec::new();
        let mut pos = input;
        loop {
            match parser(pos) {
                Parsed::Some(head, tail) => {
                    re.push(head);
                    pos = tail;
                }
                Parsed::None(_) => break,
            }
        }
        Parsed::Some(re, pos)
    }
}

pub fn some_v2<T>(mut parser: impl FnMut(&str) -> Parsed<T>) -> impl FnMut(&str) -> Parsed<Vec<T>> {
    move |input| {
        let mut re = Vec::new();
        let mut pos = input;
        loop {
            match parser(pos) {
                Parsed::Some(head, tail) => {
                    re.push(head);
                    pos = tail;
                }
                Parsed::None(_) => break,
            }
        }
        Parsed::Some(re, pos)
    }
}

#[test]
fn try_it() {
    assert_eq!(some_v1(achar('#'))("##comment").unwrap(), (vec!['#', '#'], "comment"));
    assert_eq!(some_v2(achar('#'))("##comment").unwrap(), (vec!['#', '#'], "comment"));
}

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

Теперь я не знаю, какую версию следует отдать предпочтение. Версия 1 использует Fn, который является менее общим, но версия 2 нуждается в изменяемом параметре.

Какой из них более идиоматичен / должен использоваться и каково обоснование?


Обновление: Спасибо jplatte за предложение по первой версии. Я обновил код здесь, в этом случае я нахожу еще более интересным.

1 Ответ

1 голос
/ 18 февраля 2020

Сравнение some_v1 и some_v2 в том виде, в котором вы их написали, я бы сказал, что версию 2 определенно следует отдать предпочтение, поскольку она носит более общий характер. Я не могу придумать хорошего примера для закрытия синтаксического анализа, которое бы реализовывало FnMut, но не Fn, но на самом деле нет никакого недостатка в том, чтобы parser был mut - как отмечено в первом комментарии к вашему вопросу, это не Не ограничивайте вызывающего абонента каким-либо образом.

Однако есть способ, которым вы можете сделать версию 1 более общей (не строго более общей, только частично), чем версию 2, и это путем возврата impl Fn(&str) -> … вместо impl FnMut(&str) -> …. Сделав это, вы получите две функции, каждая из которых в некоторой степени менее ограничена, чем другая, поэтому может даже иметь смысл сохранить обе:

  • Версия 1 с изменением типа возврата будет более ограничительной в своем аргументе (вызываемый не может изменить свои связанные данные), но менее ограничительный в своем типе возврата (вы гарантируете, что возвращаемый вызываемый не изменяет связанные с ним данные)
  • Версия 2 будет менее ограничительной в своем аргумент (вызываемый может изменять свои связанные данные), но более ограниченный в своем типе возврата (возвращаемый вызываемый может изменять свои связанные данные)
...