Put closure inside a union

131 Views Asked by At

I'm trying to implement a parser combinator in rust.

I have the following parser, which is built with no errors/warnings:

use std::char;

#[derive(Debug)]
enum ParseResult<T> {
    Success(T),
    Failure(&'static str),
}

fn pchar(char_to_match: char) -> impl Fn(&str) -> ParseResult<(&str, &str)> {
    move |string: &str| match string.get(0..1) {
        Some(found) => match found.chars().next().unwrap() == char_to_match {
            true => ParseResult::Success((found, string.get(1..).unwrap())),
            false => ParseResult::Failure("char didnt match"),
        },
        None => ParseResult::Failure("No more left to parse."),
    }
}

fn main() {
    println!("{:?}", ParseResult::Success(&3));
    println!("{:?}", pchar('s')("r"));
    println!("{:?}", (|(a, b)| (a, b))((2, 3)));
    let input_abc = "ABC";
    println!("{:?}", pchar('A')(input_abc));

Instead of returning a function (closure) from pchar, I want to return a Parser<T> which I have declared as:

union Parser<T> {
    func: dyn Fn(&str) -> ParseResult<(T, &str)>,
}

Then my second iteration of pchar can return a Parser<T> such as:

fn pchar2(char_to_match: char) -> Parser<&str> {
    Parser {
        func: |string: &str| match string.get(0..1) {
            Some(found) => match found.chars().next().unwrap() == char_to_match {
                true => ParseResult::Success((found, string.get(1..).unwrap())),
                false => ParseResult::Failure("char didnt match"),
            },
            None => ParseResult::Failure("No more left to parse."),
        },
    }
}

I get all sorts of warnings regarding lifetime parameters, size at compile-time and expected trait dyn Fn.

What do I need to learn to solve this issue?

Update: As per Aleksander Krauze's suggestion, I have updated it to use a Box.

struct Parser<T> {
    func: Box<dyn Fn(&str) -> ParseResult<(T, &str)>>,
}

fn pchar2(char_to_match: char) -> Parser<&'static str> {
    Parser {
        func: Box::new(move |string: &str| match string.get(0..1) {
            Some(found) => match found.chars().next().unwrap() == char_to_match {
                true => ParseResult::Success((found, string.get(1..).unwrap())),
                false => ParseResult::Failure("char didnt match"),
            },
            None => ParseResult::Failure("No more left to parse."),
        }),
    }
}

The error I get is that the lifetime of the variable string must outlive char_to_match.

Update 2: As per Aleksander Krauze's answer. Here is the final working version:

fn pchar2<'a>(char_to_match: char) -> Parser<'a, &'a str> {
    Parser {
        func: Box::new(move |string: &'a str| match string.char_indices().next() {
            Some((i, c)) => match c == char_to_match {
                true => ParseResult::Success((
                    string.get(..i + 1).unwrap(),
                    string.get(i + 1..).unwrap(),
                )),
                false => ParseResult::Failure("char didnt match"),
            },
            None => ParseResult::Failure("No more left to parse."),
        }),
    }
}
1

There are 1 best solutions below

3
Aleksander Krauze On

Following snippet should fix your lifetime errors. The main problem was that you tried to return &'static str, when it was only a subslice of string. Types here are a little complex, but you could try to simplify them if Parser wouldn't be generic over T (but have T = &'a str). I don't know however what is your more general usecase, so it might not be viable.

Note that even that this code compiles, it has a bug! In rust strings are UTF-8 encoded, so single character can take anywhere from 1 to 4 bytes. But when you slice &str you are giving bytes indexes. That could result in a panic at runtime if you would slice in the middle of a character.

#[derive(Debug)]
enum ParseResult<T> {
    Success(T),
    Failure(&'static str),
}

struct Parser<'a, T> {
    func: Box<dyn Fn(&'a str) -> ParseResult<(T, &'a str)>>,
}

fn pchar2<'a>(char_to_match: char) -> Parser<'a, &'a str> {
    Parser {
        func: Box::new(move |string: &'a str| match string.get(0..1) {
            Some(found) => match found.chars().next().unwrap() == char_to_match {
                true => ParseResult::Success((found, string.get(1..).unwrap())),
                false => ParseResult::Failure("char didnt match"),
            },
            None => ParseResult::Failure("No more left to parse."),
        }),
    }
}