๐Ÿฆ€ Functional Rust

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

Key Differences

ConceptOCamlRust
Functor syntax`map f p` (function first, like `f <$> p`)`map(p, f)` (parser first)
Generic boundsInferred by type checker`F: Fn(A) -> B + 'a` explicit
Digit conversion`Char.code c - Char.code '0'``c as u64 - '0' as u64`
Composed mapsNatural 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))
 })
}