โข Option
โข Result
โข 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
โข Option
โข Result
โข 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
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"))
| Concept | OCaml | Rust |
|---|---|---|
| Option type | `'a option` (`Some v` / `None`) | `Option<T>` (`Some(v)` / `None`) |
| Always succeeds | Yes โ `opt` converts `Error` to `Ok (None, input)` | Same โ `Err(_)` becomes `Ok((None, input))` |
| Default values | Any value, no constraint | `T: Clone` required (default may be returned many times) |
| Peek pattern | Non-consuming match | Same: return original `input`, not `rest` |
| Position reset | Immutable strings โ automatic | Explicit: 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"
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)),
})
}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)),
})
}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)),
})
}