ExamplesBy LevelBy TopicLearning Paths
156 Advanced

Optional Parser

Functional Programming

Tutorial

The Problem

Many grammar elements are optional: a sign before a number (-42 vs 42), a trailing comma, a default parameter value. The opt combinator wraps a parser in Option: if the parser succeeds, opt returns Some(value); if it fails, opt returns None (consuming no input). This makes optional grammar elements explicit and composable, avoiding nested if/else in hand-written parsers.

🎯 Learning Outcomes

  • • Understand opt as the standard way to handle optional grammar elements
  • • Learn how opt relates to many0: opt is many0 limited to at most one result
  • • See how opt enables parsing optional signs, suffixes, and modifiers
  • • Practice combining opt with map to provide default values
  • Code Example

    fn opt<'a, T: 'a>(parser: Parser<'a, T>) -> Parser<'a, Option<T>> {
        Box::new(move |input: &'a str| match parser(input) {
            Ok((value, rest)) => Ok((Some(value), rest)),
            Err(_) => Ok((None, input)),
        })
    }

    Key Differences

  • Backtracking default: Rust's simple opt implementation always backtracks on failure (tries, fails, restores input); angstrom requires explicit option combinators to allow backtracking.
  • Default values: Rust uses opt(...).map(|o| o.unwrap_or(default)); angstrom's option default parser provides the default directly.
  • Error recovery: Both return None/option default on failure; neither preserves the inner error message (failure is expected and silent).
  • Combinability: Both opt variants combine equally with map, flat_map, and sequence combinators.
  • OCaml Approach

    OCaml's angstrom provides option : 'a -> 'a t -> 'a t (provides a default value) and optional : 'a t -> 'a option t (equivalent to Rust's opt). The <?> operator adds a human-readable label. Backtracking in angstrom requires the ?>> or commit combinators — angstrom does not backtrack by default, requiring explicit backtracking markers for non-trivial alternatives.

    Full Source

    #![allow(clippy::all)]
    // Example 156: Optional Parser
    // opt: make a parser optional, returns Option<T>
    
    type ParseResult<'a, T> = Result<(T, &'a str), String>;
    type Parser<'a, T> = Box<dyn Fn(&'a str) -> ParseResult<'a, T> + 'a>;
    
    fn satisfy<'a, F>(pred: F, desc: &str) -> Parser<'a, char>
    where
        F: Fn(char) -> bool + 'a,
    {
        let desc = desc.to_string();
        Box::new(move |input: &'a str| match input.chars().next() {
            Some(c) if pred(c) => Ok((c, &input[c.len_utf8()..])),
            _ => Err(format!("Expected {}", desc)),
        })
    }
    
    fn tag<'a>(expected: &str) -> Parser<'a, &'a str> {
        let exp = expected.to_string();
        Box::new(move |input: &'a str| {
            if input.starts_with(&exp) {
                Ok((&input[..exp.len()], &input[exp.len()..]))
            } else {
                Err(format!("Expected \"{}\"", exp))
            }
        })
    }
    
    // ============================================================
    // Approach 1: opt — wrap result in Option, always succeeds
    // ============================================================
    
    fn opt<'a, T: 'a>(parser: Parser<'a, T>) -> Parser<'a, Option<T>> {
        Box::new(move |input: &'a str| match parser(input) {
            Ok((value, rest)) => Ok((Some(value), rest)),
            Err(_) => Ok((None, input)),
        })
    }
    
    // ============================================================
    // Approach 2: with_default — provide a fallback value
    // ============================================================
    
    fn with_default<'a, T: Clone + 'a>(default: T, parser: Parser<'a, T>) -> Parser<'a, T> {
        Box::new(move |input: &'a str| match parser(input) {
            Ok(result) => Ok(result),
            Err(_) => Ok((default.clone(), input)),
        })
    }
    
    // ============================================================
    // Approach 3: peek — check without consuming
    // ============================================================
    
    fn peek<'a, T: Clone + 'a>(parser: Parser<'a, T>) -> Parser<'a, Option<T>> {
        Box::new(move |input: &'a str| match parser(input) {
            Ok((value, _)) => Ok((Some(value), input)), // don't advance!
            Err(_) => Ok((None, input)),
        })
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_opt_some() {
            let p = opt(tag("+"));
            let (val, rest) = p("+42").unwrap();
            assert_eq!(val, Some("+"));
            assert_eq!(rest, "42");
        }
    
        #[test]
        fn test_opt_none() {
            let p = opt(tag("+"));
            let (val, rest) = p("42").unwrap();
            assert_eq!(val, None);
            assert_eq!(rest, "42");
        }
    
        #[test]
        fn test_opt_always_succeeds() {
            let p = opt(tag("xyz"));
            assert!(p("abc").is_ok());
            assert!(p("").is_ok());
        }
    
        #[test]
        fn test_with_default_present() {
            let p = with_default('+', satisfy(|c| c == '+' || c == '-', "sign"));
            assert_eq!(p("-5"), Ok(('-', "5")));
        }
    
        #[test]
        fn test_with_default_absent() {
            let p = with_default('+', satisfy(|c| c == '+' || c == '-', "sign"));
            assert_eq!(p("5"), Ok(('+', "5")));
        }
    
        #[test]
        fn test_peek_success_no_consume() {
            let p = peek(satisfy(|c| c.is_ascii_digit(), "digit"));
            let (val, rest) = p("123").unwrap();
            assert_eq!(val, Some('1'));
            assert_eq!(rest, "123"); // NOT consumed
        }
    
        #[test]
        fn test_peek_failure() {
            let p = peek(satisfy(|c| c.is_ascii_digit(), "digit"));
            let (val, rest) = p("abc").unwrap();
            assert_eq!(val, None);
            assert_eq!(rest, "abc");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_opt_some() {
            let p = opt(tag("+"));
            let (val, rest) = p("+42").unwrap();
            assert_eq!(val, Some("+"));
            assert_eq!(rest, "42");
        }
    
        #[test]
        fn test_opt_none() {
            let p = opt(tag("+"));
            let (val, rest) = p("42").unwrap();
            assert_eq!(val, None);
            assert_eq!(rest, "42");
        }
    
        #[test]
        fn test_opt_always_succeeds() {
            let p = opt(tag("xyz"));
            assert!(p("abc").is_ok());
            assert!(p("").is_ok());
        }
    
        #[test]
        fn test_with_default_present() {
            let p = with_default('+', satisfy(|c| c == '+' || c == '-', "sign"));
            assert_eq!(p("-5"), Ok(('-', "5")));
        }
    
        #[test]
        fn test_with_default_absent() {
            let p = with_default('+', satisfy(|c| c == '+' || c == '-', "sign"));
            assert_eq!(p("5"), Ok(('+', "5")));
        }
    
        #[test]
        fn test_peek_success_no_consume() {
            let p = peek(satisfy(|c| c.is_ascii_digit(), "digit"));
            let (val, rest) = p("123").unwrap();
            assert_eq!(val, Some('1'));
            assert_eq!(rest, "123"); // NOT consumed
        }
    
        #[test]
        fn test_peek_failure() {
            let p = peek(satisfy(|c| c.is_ascii_digit(), "digit"));
            let (val, rest) = p("abc").unwrap();
            assert_eq!(val, None);
            assert_eq!(rest, "abc");
        }
    }

    Deep Comparison

    Comparison: Example 156 — Optional Parser

    opt

    OCaml:

    let opt (p : 'a parser) : 'a option parser = fun input ->
      match p input with
      | Ok (v, rest) -> Ok (Some v, rest)
      | Error _ -> Ok (None, input)
    

    Rust:

    fn opt<'a, T: 'a>(parser: Parser<'a, T>) -> Parser<'a, Option<T>> {
        Box::new(move |input: &'a str| match parser(input) {
            Ok((value, rest)) => Ok((Some(value), rest)),
            Err(_) => Ok((None, input)),
        })
    }
    

    with_default

    OCaml:

    let with_default (default : 'a) (p : 'a parser) : 'a parser = fun input ->
      match p input with
      | Ok _ as result -> result
      | Error _ -> Ok (default, input)
    

    Rust:

    fn with_default<'a, T: Clone + 'a>(default: T, parser: Parser<'a, T>) -> Parser<'a, T> {
        Box::new(move |input: &'a str| match parser(input) {
            Ok(result) => Ok(result),
            Err(_) => Ok((default.clone(), input)),
        })
    }
    

    peek

    OCaml:

    let peek (p : 'a parser) : 'a option parser = fun input ->
      match p input with
      | Ok (v, _) -> Ok (Some v, input)  (* don't advance *)
      | Error _ -> Ok (None, input)
    

    Rust:

    fn peek<'a, T: Clone + 'a>(parser: Parser<'a, T>) -> Parser<'a, Option<T>> {
        Box::new(move |input: &'a str| match parser(input) {
            Ok((value, _)) => Ok((Some(value), input)), // don't advance
            Err(_) => Ok((None, input)),
        })
    }
    

    Exercises

  • Build a signed integer parser: opt(char_parser('-')).map(...) combined with a digit sequence.
  • Implement with_default<T: Clone>(default: T, p: Parser<T>) -> Parser<T> using opt and map.
  • Write a parser for optional trailing commas in a list: "[1, 2, 3,]" and "[1, 2, 3]" both succeed.
  • Open Source Repos