简体   繁体   中英

How do I write combinators for my own parsers in Rust?

Inspired by this video , I thought a little parser combinator library would be a good way to learn about strings, borrowing and typing in Rust—and it was so far.

I managed to get a char parser and a digit parser to work:

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

impl<T> Parsed<'_, T> {
    // I was neither sure with the third & before the T...
    pub fn unwrap(&self) -> (&T, &str) {
        match self {
            // ... nor with the first one here.
            Parsed::Some(head, tail) => (&head, &tail),
            _ => panic!("Called unwrap on nothing."),
        }
    // But this was the only way that I came up with that compiled.
    }

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

pub fn parse<T>(what: fn(&str) -> Parsed<T>, input: &str) -> Parsed<T> {
    what(input)
}

pub fn char(input: &str) -> Parsed<char> {
    match input.chars().next() {
        Some(c) => Parsed::Some(c, &input[1..]),
        None => Parsed::None(input),
    }
}

pub fn digit(input: &str) -> Parsed<u8> {
    match input.chars().next() {
        Some(d @ '0'..='9') => Parsed::Some(d as u8 - ('0' as u8), &input[1..]),
        _ => Parsed::None(input),
    }
}

Then I wanted to turn to combinators, here some to get an arbitrary number of matches for a given parser. That one hit me hard. This is the version I had in the beginning that was able to fulfill some unit tests:

pub fn some<T>(input: &str, parser: fn(&str) -> Parsed<T>) -> Parsed<Vec<T>> {
    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)
}

But to be able to use it with parse::parse it has to take only a parser function and return one. I tried so many variants:

  • fn(&str) -> Parsed<T> as return type
  • impl Fn(&str) -> Parsed<T> as return type
  • impl FnOnce(&str) -> Parsed<T> as return type
  • several for<'r> something which the compiler spat out and I don't even understand
  • packing the code into a closure and returning that, with and without move

There was always at least one line that Rust wasn't happy with. Now I don't know what to try anymore. The testing code looks like this:


#[test]
fn test() {
    assert_eq!(char("foo").unwrap(), (&'f', "oo"));
    assert!(parse(digit, "foo").is_none());
    assert_eq!(parse(digit, "9foo").unwrap(), (&9, "foo"));
    assert_eq!(
        parse(some(digit), "12space").unwrap(),
        (&vec![1, 2], "space")
    );
}

Here's a link to a playground .

Return an anonymous type that implements one of the Fn* traits by returning a closure:

fn some<T>(parser: impl Fn(&str) -> Parsed<T>) -> impl FnOnce(&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)
    }
}

Playground

Note that I've switched from function pointers to generic types for the arguments:

fn some<T>(parser: fn(&str) -> Parsed<T>) // before
fn some<T>(parser: impl Fn(&str) -> Parsed<T>) // after

I advocate doing this for all of your functions so that you have a consistent and connectable API. This is the pattern taken by many parsing libraries, including my own peresil .

See also:

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM