Optional Parser
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
opt as the standard way to handle optional grammar elementsopt relates to many0: opt is many0 limited to at most one resultopt enables parsing optional signs, suffixes, and modifiersopt with map to provide default valuesCode 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
opt implementation always backtracks on failure (tries, fails, restores input); angstrom requires explicit option combinators to allow backtracking.opt(...).map(|o| o.unwrap_or(default)); angstrom's option default parser provides the default directly.None/option default on failure; neither preserves the inner error message (failure is expected and silent).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");
}
}#[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
opt(char_parser('-')).map(...) combined with a digit sequence.with_default<T: Clone>(default: T, p: Parser<T>) -> Parser<T> using opt and map."[1, 2, 3,]" and "[1, 2, 3]" both succeed.