Map Parser
Tutorial
The Problem
Parsers produce raw strings and characters, but applications need structured data — integers, enums, structs. The map combinator transforms a parser's output without changing what it consumes. It is the Functor operation for parsers: if you have a Parser<A> and a function A -> B, map produces a Parser<B>. This functional transformation step keeps parsing (what to consume) separate from interpretation (what it means), a core principle of combinator-based parsing.
🎯 Learning Outcomes
map as the fundamental output-transformation combinator (Functor)map separates parsing from interpretationmapmap with other combinators to build typed parsersCode Example
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))
})
}Key Differences
>>| (or <$>) applies map with operator precedence; Rust uses method chaining or named map(p, f) functions.lift2 f p q = pair(p, q).map(|(a, b)| f(a, b)) in Rust; OCaml's syntax is more concise for multi-parser transformations.map's function f is never called on failure.map is the foundational combinator — all structured parsers are built from primitives via map and sequencing.OCaml Approach
OCaml's angstrom provides map : ('a -> 'b) -> 'a t -> 'b t and the infix >>|:
let uint_parser = many1 digit >>| (fun cs -> int_of_string (String.of_list cs))
OCaml's |> and >>| operators compose naturally. The lift family (lift2, lift3) applies functions of multiple arguments to multiple parsers simultaneously, avoiding explicit pair + map combinations.
Full Source
#![allow(clippy::all)]
// 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))
})
}
#[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, "")));
}
}#[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, "")));
}
}
Deep Comparison
Comparison: Example 159 — Map Parser
map
OCaml:
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:
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:
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))
})
}
Exercises
digit_value() -> Parser<u32> that parses a single ASCII digit and returns its numeric value using map.color_parser() -> Parser<Color> where Color is an enum {Red, Green, Blue} and inputs are "red", "green", "blue".map_err<T>(p: Parser<T>, f: impl Fn(String) -> String) -> Parser<T> that transforms error messages.