How does nom::combinator::map transform parser results and when would you use it over nom::combinator::map_res?

nom::combinator::map applies a transformation function to the output of a parser when the parser succeeds, allowing you to convert the parsed value into a different type without affecting the parsing logic. nom::combinator::map_res does the same thing but the transformation function returns a Result, enabling the transformation itself to fail and propagate an error. Use map when your transformation is infallible (e.g., converting between types, wrapping in structs), and use map_res when the transformation might fail (e.g., validation, parsing strings into numbers, checking constraints).

Basic map Usage

use nom::{
    IResult,
    bytes::complete::tag,
    combinator::map,
};
 
// Parse a string tag and convert to a number
fn parse_number(input: &str) -> IResult<&str, u32> {
    map(tag("42"), |_s: &str| 42u32)(input)
}
 
fn main() {
    let result = parse_number("42abc");
    println!("{:?}", result);
    // Ok(("abc", 42))
}

map applies a function to transform the parser's output.

Transforming String Output

use nom::{
    IResult,
    bytes::complete::take_while,
    combinator::map,
    character::is_alphabetic,
};
 
// Parse a name and convert to owned String
fn parse_name(input: &str) -> IResult<&str, String> {
    map(
        take_while(|c: char| c.is_alphabetic()),
        |s: &str| s.to_string(),
    )(input)
}
 
fn main() {
    let result = parse_name("hello world");
    println!("{:?}", result);
    // Ok((" world", "hello"))
}

map converts the parsed string slice into an owned String.

Wrapping in Structs

use nom::{
    IResult,
    bytes::complete::tag,
    combinator::map,
};
 
#[derive(Debug)]
struct Keyword(String);
 
// Parse a keyword and wrap in a struct
fn parse_keyword(input: &str) -> IResult<&str, Keyword> {
    map(tag("fn"), |s: &str| Keyword(s.to_string()))(input)
}
 
fn main() {
    let result = parse_keyword("fn foo()");
    println!("{:?}", result);
    // Ok((" foo()", Keyword("fn")))
}

Use map to wrap parsed values into domain-specific types.

Multiple Fields with map

use nom::{
    IResult,
    bytes::complete::tag,
    combinator::map,
    sequence::tuple,
    character::complete::digit1,
};
 
#[derive(Debug)]
struct Point {
    x: u32,
    y: u32,
}
 
// Parse coordinates and create a Point
fn parse_point(input: &str) -> IResult<&str, Point> {
    map(
        tuple((digit1, tag(","), digit1)),
        |(x, _, y): (&str, &str, &str)| Point {
            x: x.parse().unwrap(),
            y: y.parse().unwrap(),
        },
    )(input)
}
 
fn main() {
    let result = parse_point("10,20extra");
    println!("{:?}", result);
    // Ok(("extra", Point { x: 10, y: 20 }))
}

map combines multiple parsed values into a single struct.

map_res for Fallible Transformations

use nom::{
    IResult,
    bytes::complete::digit1,
    combinator::map_res,
    error::Error,
};
 
// Parse digits and convert to number (may fail)
fn parse_u32(input: &str) -> IResult<&str, u32, Error<&str>> {
    map_res(digit1, |s: &str| s.parse::<u32>())(input)
}
 
fn main() {
    let result = parse_u32("123abc");
    println!("{:?}", result);
    // Ok(("abc", 123))
    
    // Overflow fails
    let result = parse_u32("999999999999999999999");
    println!("{:?}", result);
    // Err(Error(...)) - number too large
}

map_res allows the transformation to return a Result.

Validation with map_res

use nom::{
    IResult,
    bytes::complete::digit1,
    combinator::map_res,
    error::{Error, ErrorKind},
};
 
// Parse a positive number and validate it's within range
fn parse_percentage(input: &str) -> IResult<&str, u8, Error<&str>> {
    map_res(
        digit1,
        |s: &str| -> Result<u8, &'static str> {
            let n: u32 = s.parse().map_err(|_| "parse failed")?;
            if n > 100 {
                Err("percentage must be <= 100")
            } else {
                Ok(n as u8)
            }
        },
    )(input)
}
 
fn main() {
    let result = parse_percentage("50%");
    println!("{:?}", result);
    // Ok(("%", 50))
    
    let result = parse_percentage("150%");
    println!("{:?}", result);
    // Err(Error { ... })
}

Use map_res to validate values during parsing.

Custom Error Types

use nom::{
    IResult,
    bytes::complete::digit1,
    combinator::map_res,
    error::{Error, ParseError},
};
 
#[derive(Debug)]
enum ParseError {
    InvalidNumber,
    OutOfRange,
}
 
// Parse with custom error type
fn parse_port(input: &str) -> IResult<&str, u16, Error<&str>> {
    map_res(
        digit1,
        |s: &str| -> Result<u16, nom::Err<Error<&str>>> {
            let n: u32 = s.parse().map_err(|_| {
                nom::Err::Error(Error::new(s, nom::error::ErrorKind::Digit))
            })?;
            if n > 65535 {
                return Err(nom::Err::Error(Error::new(s, nom::error::ErrorKind::Verify)));
            }
            Ok(n as u16)
        },
    )(input)
}
 
fn main() {
    let result = parse_port("8080");
    println!("{:?}", result);
    // Ok(("", 8080))
}

map_res can return custom error types for better error messages.

Infallible vs Fallible Transformations

use nom::{
    IResult,
    bytes::complete::{tag, take},
    combinator::{map, map_res},
    character::complete::digit1,
};
 
// Infallible transformation - always succeeds
fn parse_uppercase(input: &str) -> IResult<&str, String> {
    map(take(3usize), |s: &str| s.to_uppercase())(input)
}
 
// Fallible transformation - may fail
fn parse_age(input: &str) -> IResult<&str, u8> {
    map_res(digit1, |s: &str| {
        let n: u32 = s.parse()?;
        if n > 150 {
            Err("age too large")
        } else {
            Ok(n as u8)
        }
    })(input)
}
 
fn main() {
    let result = parse_uppercase("abc");
    println!("{:?}", result);  // Ok(("", "ABC"))
    
    let result = parse_age("25");
    println!("{:?}", result);  // Ok(("", 25))
    
    let result = parse_age("200");
    println!("{:?}", result);  // Err(...)
}

Use map for infallible, map_res for fallible transformations.

String to Number Parsing

use nom::{
    IResult,
    bytes::complete::digit1,
    combinator::map_res,
};
 
fn parse_int(input: &str) -> IResult<&str, i32> {
    map_res(digit1, |s: &str| s.parse::<i32>())(input)
}
 
fn parse_hex(input: &str) -> IResult<&str, u32> {
    map_res(
        nom::bytes::complete::take_while(|c: char| c.is_ascii_hexdigit()),
        |s: &str| u32::from_str_radix(s, 16),
    )(input)
}
 
fn main() {
    let result = parse_int("123abc");
    println!("{:?}", result);  // Ok(("abc", 123))
    
    let result = parse_hex("ff00abc");
    println!("{:?}", result);  // Ok(("abc", 65280))
}

map_res is commonly used for string-to-number conversion.

Combining Multiple Parsers

use nom::{
    IResult,
    bytes::complete::tag,
    combinator::map,
    sequence::tuple,
};
 
#[derive(Debug)]
struct KeyValue {
    key: String,
    value: String,
}
 
fn parse_key_value(input: &str) -> IResult<&str, KeyValue> {
    map(
        tuple((
            nom::bytes::complete::take_while(|c: char| c.is_alphabetic()),
            tag("="),
            nom::bytes::complete::take_while(|c: char| c.is_alphanumeric()),
        )),
        |(key, _, value): (&str, &str, &str)| KeyValue {
            key: key.to_string(),
            value: value.to_string(),
        },
    )(input)
}
 
fn main() {
    let result = parse_key_value("name=alice rest");
    println!("{:?}", result);
    // Ok((" rest", KeyValue { key: "name", value: "alice" }))
}

map combines results from tuple into a structured type.

Parsing with Context

use nom::{
    IResult,
    bytes::complete::tag,
    combinator::{map, map_res},
    sequence::preceded,
};
 
enum Value {
    Number(u32),
    String(String),
}
 
fn parse_value(input: &str) -> IResult<&str, Value> {
    // Try number first
    let (input, _) = tag("value:")(input)?;
    
    map_res(
        nom::bytes::complete::take_while(|c: char| c.is_digit(10)),
        |s: &str| -> Result<Value, &'static str> {
            s.parse::<u32>()
                .map(Value::Number)
                .or_else(|_| {
                    // If parsing fails, treat as string
                    // This approach has issues - see better version below
                    Err("not a number")
                })
        },
    )(input)
}
 
// Better approach: try alternatives
fn parse_value_better(input: &str) -> IResult<&str, Value> {
    let (input, _) = tag("value:")(input)?;
    
    nom::branch::alt((
        map_res(
            nom::character::complete::digit1,
            |s: &str| s.parse::<u32>().map(Value::Number),
        ),
        map(
            nom::bytes::complete::take_while(|c: char| c.is_alphabetic()),
            |s: &str| Value::String(s.to_string()),
        ),
    ))(input)
}

Use alt with map and map_res for alternatives.

Error Handling in map_res

use nom::{
    IResult,
    bytes::complete::digit1,
    combinator::map_res,
    error::{Error, ErrorKind, ParseError},
};
 
#[derive(Debug)]
enum ValidationError {
    ParseError,
    TooLarge,
}
 
fn parse_byte(input: &str) -> IResult<&str, u8, Error<&str>> {
    map_res(digit1, |s: &str| {
        let n: u32 = s.parse().map_err(|_| ValidationError::ParseError)?;
        if n > 255 {
            Err(ValidationError::TooLarge)
        } else {
            Ok(n as u8)
        }
    })(input)
    .map_err(|e: nom::Err<Error<&str>>| {
        // Transform error type if needed
        e
    })
}
 
fn main() {
    let result = parse_byte("300");
    println!("{:?}", result);  // Err(...)
}

map_res propagates transformation errors through the parser.

map_parser for Nested Parsing

use nom::{
    IResult,
    bytes::complete::take,
    combinator::map_parser,
};
 
// Parse bytes, then parse the result
fn parse_nested(input: &str) -> IResult<&str, u32> {
    // Take 3 characters, then parse them as a number
    map_parser(take(3usize), |s: &str| {
        s.parse::<u32>().map(|n| Ok(("", n)))
            .unwrap_or(Err(nom::error::Error::new(s, nom::error::ErrorKind::Digit)))
    })(input)
}
 
// Using map with map_parser
fn parse_and_transform(input: &str) -> IResult<&str, String> {
    map(
        map_parser(take(3usize), parse_three_digits),
        |n| format!("value: {}", n),
    )(input)
}
 
fn parse_three_digits(input: &str) -> IResult<&str, u32> {
    nom::character::complete::digit1(input)
        .and_then(|(remaining, digits)| {
            digits.parse::<u32>()
                .map(|n| Ok((remaining, n)))
                .unwrap_or(Err(nom::error::Error::new(input, nom::error::ErrorKind::Digit)))
        })
}
 
fn main() {
    let result = parse_nested("123rest");
    println!("{:?}", result);
}

map_parser applies a parser to the output of another parser.

Performance Considerations

use nom::{
    IResult,
    bytes::complete::{tag, take},
    combinator::{map, map_res},
};
 
// map is slightly faster for infallible transformations
fn fast_parse(input: &str) -> IResult<&str, String> {
    map(take(5usize), |s: &str| s.to_string())(input)
}
 
// map_res has overhead for Result handling
fn slower_parse(input: &str) -> IResult<&str, u32> {
    map_res(take(5usize), |s: &str| s.parse())(input)
}
 
// Prefer map when transformation cannot fail
fn preferred_parse(input: &str) -> IResult<&str, String> {
    map(tag("constant"), |_| String::from("CONSTANT"))(input)
}

Use map for infallible transformations to avoid Result overhead.

Complete Parsing Example

use nom::{
    IResult,
    bytes::complete::{tag, take_while},
    combinator::{map, map_res},
    sequence::tuple,
    character::complete::{digit1, char},
};
 
#[derive(Debug)]
struct Person {
    name: String,
    age: u8,
}
 
fn parse_name(input: &str) -> IResult<&str, String> {
    map(
        take_while(|c: char| c.is_alphabetic()),
        |s: &str| s.to_string(),
    )(input)
}
 
fn parse_age(input: &str) -> IResult<&str, u8> {
    map_res(digit1, |s: &str| {
        let n: u32 = s.parse()?;
        if n > 150 {
            Err("age too large")
        } else {
            Ok(n as u8)
        }
    })(input)
}
 
fn parse_person(input: &str) -> IResult<&str, Person> {
    map(
        tuple((
            parse_name,
            tag(","),
            parse_age,
        )),
        |(name, _, age)| Person { name, age },
    )(input)
}
 
fn main() {
    let result = parse_person("Alice,30 rest");
    println!("{:?}", result);
    // Ok((" rest", Person { name: "Alice", age: 30 }))
    
    let result = parse_person("Bob,200 rest");
    println!("{:?}", result);
    // Err(...) - age too large
}

A complete parser combining map and map_res.

Comparison Table

Aspect map map_res
Transformation Infallible (always succeeds) Fallible (may return Err)
Function signature Fn(T) -> U Fn(T) -> Result<U, E>
Error handling Cannot fail Propagates errors
Use case Type conversion, wrapping Validation, parsing strings
Performance Slightly faster Has Result overhead

Synthesis

Use map when:

  • Converting between types (e.g., &str to String)
  • Wrapping in structs or enums
  • Infallible transformations
  • Simple arithmetic operations
  • Creating domain types from parsed data

Use map_res when:

  • Parsing strings into numbers (may overflow)
  • Validating constraints (e.g., range checks)
  • Any fallible transformation
  • Need custom error messages
  • Transformation requires validation

Key differences:

  • map: Transformation function returns U directly
  • map_res: Transformation function returns Result<U, E>
  • map is infallible, map_res can propagate errors
  • map has slightly better performance

Best practices:

  • Use map for all infallible transformations
  • Use map_res when transformation can fail
  • Combine with alt for multiple parsing strategies
  • Use custom error types for better error messages

Key insight: map transforms parser output while map_res transforms and validates. Both compose cleanly with other nom combinators, but map_res integrates error handling into the transformation. Use map for type conversions and map_res when the transformation itself can fail.