159: Map Parser
Difficulty: โญโญโญ Level: Foundations `map(parser, f)` transforms the parsed value with a function โ turning raw characters and strings into typed data.The Problem This Solves
Parsing produces raw text. What you actually want is typed data. You parse `"42"` but you want the number `42`. You parse `"true"` but you want the boolean `true`. You parse a list of digit characters `['1', '2', '3']` but you want the integer `123`. Every parser eventually needs to convert its raw output into something useful. Without `map`, you'd run the parser, get the result, then manually transform it outside the parser. That means you can't compose โ the transformation isn't part of the parser chain. With `map`, the transformation lives inside the parser, so you can pass a `Parser<u64>` to anything expecting a parser, not a `Parser<Vec<char>>` that needs manual post-processing. `map` is what makes parsers functors โ a concept from functional programming that means "you can transform the inside value without changing the structure." Same parser, same position tracking, different output type.The Intuition
`map` is `Iterator::map` but for parsers. You already know `vec.iter().map(|x| x * 2)` โ it applies a function to each element without touching the collection structure. `map(parser, f)` does the same: apply `f` to the parsed value, without changing what the parser consumes or how it handles errors. If the parser fails, `map` fails too โ it only runs `f` on success. The transformation is "inside" the `Result`. This makes `map` pure: it never changes parsing behavior, only the output type.How It Works in Rust
`map` โ transform the parsed value:fn map<'a, A: 'a, B: 'a, F>(parser: Parser<'a, A>, f: F) -> Parser<'a, B>
where F: Fn(A) -> B + 'a // f must be callable with A, produce B, and be capturable
{
Box::new(move |input: &'a str| {
let (value, rest) = parser(input)?; // run parser; propagate error
Ok((f(value), rest)) // transform value, keep same remainder
})
}
The generic parameters `A` and `B` let `map` work with any input/output type pair. `F: Fn(A) -> B + 'a` says "any function (or closure) from A to B."
`map_const` โ replace parsed value with a constant:
fn map_const<'a, A: 'a, B: Clone + 'a>(parser: Parser<'a, A>, value: B) -> Parser<'a, B> {
Box::new(move |input: &'a str| {
let (_, rest) = parser(input)?; // run parser, discard its value
Ok((value.clone(), rest)) // return fixed constant instead
})
}
This is how you turn `tag("true")` into a parser that returns `true` (boolean), not `"true"` (string).
`parse_nat` โ a practical composition:
fn parse_nat<'a>() -> Parser<'a, u64> {
map(
many1(satisfy(|c| c.is_ascii_digit(), "digit")), // parse: Vec<char>
|digits| digits.iter().fold(0u64, |acc, &d| {
acc * 10 + (d as u64 - '0' as u64) // convert digit char to value
}),
)
}
Step by step:
1. `satisfy(|c| c.is_ascii_digit(), "digit")` โ parses one digit char
2. `many1(...)` โ collects one or more into `Vec<char>`
3. `map(..., |digits| fold(...))` โ converts `Vec<char>` to `u64`
4. Result: a `Parser<u64>` โ fully typed, fully composed
Usage:
// char โ uppercase
let p = map(satisfy(|c| c.is_ascii_lowercase(), "lower"), |c| c.to_ascii_uppercase());
println!("{:?}", p("abc")); // Ok(('A', "bc"))
// "true" โ true (boolean)
let p = map_const(tag("true"), true);
println!("{:?}", p("true!")); // Ok((true, "!"))
// digit string โ u64
let p = parse_nat();
println!("{:?}", p("42rest")); // Ok((42, "rest"))
// Compose maps: char โ u32 โ u32 (doubled)
let p = map(
map(satisfy(|c| c.is_ascii_digit(), "digit"), |c| c as u32 - '0' as u32),
|n| n * 2,
);
println!("{:?}", p("5")); // Ok((10, ""))
What This Unlocks
- Typed parse results โ your parsers return `u64`, `bool`, `String`, `Vec<Token>` โ not `char` or `&str`.
- Composition without breaking the chain โ transformations live inside the parser, so you can pass `parse_nat()` wherever a `Parser<u64>` is expected.
- The `parse_nat` pattern is used in examples 160, 161, and 162 โ it's the standard way to parse numbers in the entire series.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Functor syntax | `map f p` (function first, like `f <$> p`) | `map(p, f)` (parser first) |
| Generic bounds | Inferred by type checker | `F: Fn(A) -> B + 'a` explicit |
| Digit conversion | `Char.code c - Char.code '0'` | `c as u64 - '0' as u64` |
| Composed maps | Natural with currying: `map (map p f) g` | Explicit nesting: `map(map(p, f), g)` |
| Constant replacement | `map (fun _ -> v) p` | `map_const(p, v)` |
// Example 159: Map Parser
// map: transform parser output functorially
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 many1<'a, T: 'a>(parser: Parser<'a, T>) -> Parser<'a, Vec<T>> {
Box::new(move |input: &'a str| {
let (first, mut rem) = parser(input)?;
let mut results = vec![first];
while let Ok((v, rest)) = parser(rem) {
results.push(v);
rem = rest;
}
Ok((results, rem))
})
}
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: map โ transform parsed value
// ============================================================
fn map<'a, A: 'a, B: 'a, F>(parser: Parser<'a, A>, f: F) -> Parser<'a, B>
where F: Fn(A) -> B + 'a {
Box::new(move |input: &'a str| {
let (value, rest) = parser(input)?;
Ok((f(value), rest))
})
}
// ============================================================
// Approach 2: map2 โ combine two parser results
// ============================================================
fn map2<'a, A: 'a, B: 'a, C: 'a, F>(
p1: Parser<'a, A>, p2: Parser<'a, B>, f: F,
) -> Parser<'a, C>
where F: Fn(A, B) -> C + 'a {
Box::new(move |input: &'a str| {
let (v1, rest) = p1(input)?;
let (v2, rem) = p2(rest)?;
Ok((f(v1, v2), rem))
})
}
// ============================================================
// Approach 3: map_const โ ignore result, return fixed value
// ============================================================
fn map_const<'a, A: 'a, B: Clone + 'a>(parser: Parser<'a, A>, value: B) -> Parser<'a, B> {
Box::new(move |input: &'a str| {
let (_, rest) = parser(input)?;
Ok((value.clone(), rest))
})
}
/// Parse natural number: one or more digits โ u64
fn parse_nat<'a>() -> Parser<'a, u64> {
map(
many1(satisfy(|c| c.is_ascii_digit(), "digit")),
|digits| digits.iter().fold(0u64, |acc, &d| acc * 10 + (d as u64 - '0' as u64)),
)
}
fn main() {
println!("=== map: char to uppercase ===");
let p = map(satisfy(|c| c.is_ascii_lowercase(), "lower"), |c| c.to_ascii_uppercase());
println!("{:?}", p("abc")); // Ok(('A', "bc"))
println!("\n=== parse_nat ===");
let p = parse_nat();
println!("{:?}", p("42rest")); // Ok((42, "rest"))
println!("{:?}", p("0")); // Ok((0, ""))
println!("\n=== map_const ===");
let p = map_const(tag("true"), true);
println!("{:?}", p("true!")); // Ok((true, "!"))
println!("\nโ All examples completed");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_map_uppercase() {
let p = map(satisfy(|c| c.is_ascii_lowercase(), "lower"), |c| c.to_ascii_uppercase());
assert_eq!(p("abc"), Ok(('A', "bc")));
}
#[test]
fn test_map_preserves_error() {
let p = map(satisfy(|c| c.is_ascii_digit(), "digit"), |c| c as u32);
assert!(p("abc").is_err());
}
#[test]
fn test_parse_nat() {
let p = parse_nat();
assert_eq!(p("42rest"), Ok((42, "rest")));
}
#[test]
fn test_parse_nat_zero() {
let p = parse_nat();
assert_eq!(p("0"), Ok((0, "")));
}
#[test]
fn test_parse_nat_fail() {
let p = parse_nat();
assert!(p("abc").is_err());
}
#[test]
fn test_map2() {
let p = map2(
satisfy(|c| c.is_ascii_digit(), "digit"),
satisfy(|c| c.is_ascii_digit(), "digit"),
|a, b| format!("{}{}", a, b),
);
assert_eq!(p("12x"), Ok(("12".to_string(), "x")));
}
#[test]
fn test_map_const_true() {
let p = map_const(tag("true"), true);
assert_eq!(p("true!"), Ok((true, "!")));
}
#[test]
fn test_map_const_false() {
let p = map_const(tag("false"), false);
assert_eq!(p("false"), Ok((false, "")));
}
#[test]
fn test_map_chain() {
// map(map(p, f), g) == map(p, |x| g(f(x)))
let p = map(
map(satisfy(|c| c.is_ascii_digit(), "digit"), |c| c as u32 - '0' as u32),
|n| n * 2,
);
assert_eq!(p("5"), Ok((10, "")));
}
}
(* Example 159: Map Parser *)
(* map: transform parser output functorially *)
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 many0 (p : 'a parser) : 'a list parser = fun input ->
let rec go acc rem =
match p rem with
| Ok (v, rest) -> go (v :: acc) rest
| Error _ -> Ok (List.rev acc, rem)
in go [] input
let many1 (p : 'a parser) : 'a list parser = fun input ->
match p input with
| Error e -> Error e
| Ok (v, rest) ->
match many0 p rest with
| Ok (vs, rem) -> Ok (v :: vs, rem)
| Error e -> Error e
(* Approach 1: map โ transform the parsed value *)
let map (f : 'a -> 'b) (p : 'a parser) : 'b parser = fun input ->
match p input with
| Ok (v, rest) -> Ok (f v, rest)
| Error e -> Error e
(* Approach 2: map2 โ combine two parser results *)
let map2 (f : 'a -> 'b -> 'c) (p1 : 'a parser) (p2 : 'b parser) : 'c parser = fun input ->
match p1 input with
| Error e -> Error e
| Ok (v1, rest) ->
match p2 rest with
| Error e -> Error e
| Ok (v2, rem) -> Ok (f v1 v2, rem)
(* Approach 3: map with const โ ignore result, return fixed value *)
let map_const (value : 'b) (p : 'a parser) : 'b parser = fun input ->
match p input with
| Ok (_, rest) -> Ok (value, rest)
| Error e -> Error e
(* Practical: parse digits and convert to int *)
let is_digit = satisfy (fun c -> c >= '0' && c <= '9') "digit"
let parse_nat : int parser =
map (fun chars ->
List.fold_left (fun acc c -> acc * 10 + (Char.code c - Char.code '0')) 0 chars
) (many1 is_digit)
(* Tests *)
let () =
(* map: char to uppercase *)
let upper_letter = map Char.uppercase_ascii
(satisfy (fun c -> c >= 'a' && c <= 'z') "lowercase") in
assert (upper_letter "abc" = Ok ('A', "bc"));
(* parse_nat: digits to int *)
assert (parse_nat "42rest" = Ok (42, "rest"));
assert (parse_nat "0" = Ok (0, ""));
assert (Result.is_error (parse_nat "abc"));
(* map2 *)
let digit = satisfy (fun c -> c >= '0' && c <= '9') "digit" in
let pair_str = map2 (fun a b -> Printf.sprintf "%c%c" a b) digit digit in
assert (pair_str "12x" = Ok ("12", "x"));
(* map_const *)
let tag s : string parser = fun input ->
let len = String.length s in
if String.length input >= len && String.sub input 0 len = s then
Ok (s, String.sub input len (String.length input - len))
else Error (Printf.sprintf "Expected \"%s\"" s) in
let true_parser = map_const true (tag "true") in
assert (true_parser "true!" = Ok (true, "!"));
print_endline "โ All tests passed"
๐ Detailed Comparison
Comparison: Example 159 โ Map Parser
map
OCaml:
๐ช Show OCaml equivalent
let map (f : 'a -> 'b) (p : 'a parser) : 'b parser = fun input ->
match p input with
| Ok (v, rest) -> Ok (f v, rest)
| Error e -> Error e
Rust:
fn map<'a, A: 'a, B: 'a, F>(parser: Parser<'a, A>, f: F) -> Parser<'a, B>
where F: Fn(A) -> B + 'a {
Box::new(move |input: &'a str| {
let (value, rest) = parser(input)?;
Ok((f(value), rest))
})
}Practical: parse_nat
OCaml:
๐ช Show OCaml equivalent
let parse_nat : int parser =
map (fun chars ->
List.fold_left (fun acc c -> acc * 10 + (Char.code c - Char.code '0')) 0 chars
) (many1 is_digit)
Rust:
fn parse_nat<'a>() -> Parser<'a, u64> {
map(
many1(satisfy(|c| c.is_ascii_digit(), "digit")),
|digits| digits.iter().fold(0u64, |acc, &d| acc * 10 + (d as u64 - '0' as u64)),
)
}map_const
OCaml:
๐ช Show OCaml equivalent
let map_const (value : 'b) (p : 'a parser) : 'b parser = fun input ->
match p input with
| Ok (_, rest) -> Ok (value, rest)
| Error e -> Error e
Rust:
fn map_const<'a, A: 'a, B: Clone + 'a>(parser: Parser<'a, A>, value: B) -> Parser<'a, B> {
Box::new(move |input: &'a str| {
let (_, rest) = parser(input)?;
Ok((value.clone(), rest))
})
}