๐Ÿฆ€ Functional Rust
๐ŸŽฌ Error Handling in Rust Option, Result, the ? operator, and combinators.
๐Ÿ“ Text version (for readers / accessibility)

โ€ข Option represents a value that may or may not exist โ€” Some(value) or None

โ€ข Result represents success (Ok) or failure (Err) โ€” no exceptions needed

โ€ข The ? operator propagates errors up the call stack concisely

โ€ข Combinators like .map(), .and_then(), .unwrap_or() chain fallible operations

โ€ข The compiler forces you to handle every error case โ€” no silent failures

156: Optional Parser

Difficulty: โญโญโญ Level: Foundations `opt(p)` wraps any parser to make it optional โ€” the parser equivalent of regex `?`.

The Problem This Solves

Some parts of a grammar are optional. A number might have a sign: `-42` or `+42` or just `42`. An HTTP header might have a comment in parentheses or might not. A function parameter list might be empty. Without `opt`, you'd need to handle this with awkward fallback logic everywhere: try the parser, catch the error, decide whether to continue or not. That logic is always the same โ€” "try it, use the value if it works, use `None` if it doesn't, never fail" โ€” so it belongs in a combinator. `opt` converts a parser that might fail into a parser that never fails, returning `Some(value)` on success and `None` on failure. The key property: on failure, the input is left unchanged. The optional thing simply wasn't there.

The Intuition

Think of `opt` as "try this, don't panic if it doesn't work." If you ask a friend "did you bring an umbrella?" and they say no, you don't stop the conversation โ€” you just know the umbrella isn't there. `opt` is that shrug. `peek` is a related idea: "look ahead without moving." After a successful `peek`, the input position stays where it was. You saw the next character, but didn't consume it. Useful for deciding which parser to use next without committing. `with_default` is the opposite of `None` โ€” if the optional thing wasn't there, assume a default value. Instead of `Option<T>`, you get `T` directly.

How It Works in Rust

`opt` โ€” make any parser optional:
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)),  // success: wrap in Some
     Err(_)            => Ok((None, input)),         // failure: return None, don't advance
 })
}
The `Err(_)` arm discards the error message and returns `Ok`. This is what makes `opt` always succeed. The input position is reset to `input` (not `rest`) โ€” we didn't consume anything on failure. `with_default` โ€” fallback value instead of `None`:
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)),  // use default, reset position
 })
}
`T: Clone` is required because `default` may be returned multiple times (once per parse attempt), and we need to clone it each time. `peek` โ€” lookahead 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)),  // succeeded but don't advance โ€” return original input
     Err(_)         => Ok((None, input)),
 })
}
The only difference from `opt`: on success, we return `input` (original position) instead of `rest`. The value is available but the cursor didn't move. Usage:
// Parsing optional sign prefix
let p = opt(satisfy(|c| c == '+' || c == '-', "sign"));
println!("{:?}", p("+42")); // Ok((Some('+'), "42"))
println!("{:?}", p("42"));  // Ok((None, "42")) โ€” no sign, still Ok, position unchanged

// Default sign is '+' if absent
let p = with_default('+', satisfy(|c| c == '+' || c == '-', "sign"));
println!("{:?}", p("-5")); // Ok(('-', "5"))
println!("{:?}", p("5"));  // Ok(('+', "5")) โ€” defaulted to '+'

// Peek ahead without consuming
let p = peek(satisfy(|c| c.is_ascii_digit(), "digit"));
println!("{:?}", p("123")); // Ok((Some('1'), "123")) โ€” '1' not consumed!
println!("{:?}", p("abc")); // Ok((None, "abc"))

What This Unlocks

Key Differences

ConceptOCamlRust
Option type`'a option` (`Some v` / `None`)`Option<T>` (`Some(v)` / `None`)
Always succeedsYes โ€” `opt` converts `Error` to `Ok (None, input)`Same โ€” `Err(_)` becomes `Ok((None, input))`
Default valuesAny value, no constraint`T: Clone` required (default may be returned many times)
Peek patternNon-consuming matchSame: return original `input`, not `rest`
Position resetImmutable strings โ€” automaticExplicit: discard `rest`, return `input`
// 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)),
    })
}

fn main() {
    println!("=== opt ===");
    let p = opt(tag("+"));
    println!("{:?}", p("+42")); // Ok((Some("+"), "42"))
    println!("{:?}", p("42"));  // Ok((None, "42"))

    println!("\n=== with_default ===");
    let p = with_default('+', satisfy(|c| c == '+' || c == '-', "sign"));
    println!("{:?}", p("+5")); // Ok(('+', "5"))
    println!("{:?}", p("5"));  // Ok(('+', "5"))

    println!("\n=== peek ===");
    let p = peek(satisfy(|c| c.is_ascii_digit(), "digit"));
    println!("{:?}", p("123")); // Ok((Some('1'), "123")) โ€” not consumed
    println!("{:?}", p("abc")); // Ok((None, "abc"))

    println!("\nโœ“ All examples completed");
}

#[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");
    }
}
(* Example 156: Optional Parser *)
(* opt: make a parser optional, returns Option *)

type 'a parse_result = ('a * string, string) result
type 'a parser = string -> 'a parse_result

let satisfy pred desc : char parser = fun input ->
  if String.length input > 0 && pred input.[0] then
    Ok (input.[0], String.sub input 1 (String.length input - 1))
  else Error (Printf.sprintf "Expected %s" desc)

let tag expected : string parser = fun input ->
  let len = String.length expected in
  if String.length input >= len && String.sub input 0 len = expected then
    Ok (expected, String.sub input len (String.length input - len))
  else Error (Printf.sprintf "Expected \"%s\"" expected)

(* Approach 1: opt โ€” wrap result in option, always succeeds *)
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)

(* Approach 2: with_default โ€” provide a fallback value *)
let with_default (default : 'a) (p : 'a parser) : 'a parser = fun input ->
  match p input with
  | Ok _ as result -> result
  | Error _ -> Ok (default, input)

(* Approach 3: peek โ€” check if parser would succeed without consuming *)
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)

(* Tests *)
let () =
  let digit = satisfy (fun c -> c >= '0' && c <= '9') "digit" in

  (* opt: success case *)
  assert (opt (tag "+") "+42" = Ok (Some "+", "42"));
  (* opt: failure returns None without consuming *)
  assert (opt (tag "+") "42" = Ok (None, "42"));

  (* with_default *)
  assert (with_default '+' (satisfy (fun c -> c = '+' || c = '-') "sign") "+5" = Ok ('+', "5"));
  assert (with_default '+' (satisfy (fun c -> c = '+' || c = '-') "sign") "5" = Ok ('+', "5"));

  (* peek *)
  (match peek digit "123" with
   | Ok (Some '1', "123") -> ()  (* input not consumed *)
   | _ -> failwith "Peek test failed");

  (match peek digit "abc" with
   | Ok (None, "abc") -> ()
   | _ -> failwith "Peek test 2 failed");

  print_endline "โœ“ All tests passed"

๐Ÿ“Š Detailed Comparison

Comparison: Example 156 โ€” Optional Parser

opt

OCaml:

๐Ÿช Show OCaml equivalent
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:

๐Ÿช Show OCaml equivalent
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:

๐Ÿช Show OCaml equivalent
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)),
 })
}