What are the differences between nom::combinator::map and nom::combinator::map_res for transforming parser output?
nom::combinator::map applies an infallible transformation to parser output, converting the successful parse result to a new type without any possibility of failure. nom::combinator::map_res applies a fallible transformation that can fail, returning a Result from the transformation functionâif the function returns Err, the parser backtracks and fails as if the parse itself had failed. This distinction matters when parsing requires validation or conversion: map is for pure transformations like converting types, while map_res is for transformations that might fail like parsing strings to integers or validating parsed values. Using map_res allows the transformation failure to integrate with nom's backtracking system, enabling alternative parsers to be tried.
Basic map Usage
use nom::{IResult, bytes::complete::tag, combinator::map};
fn main() {
// map: apply infallible transformation
let input = "hello world";
let parser = map(tag("hello"), |s: &str| s.to_uppercase());
let result: IResult<&str, String> = parser(input);
match result {
Ok((remaining, output)) => {
println!("Output: {}", output); // "HELLO"
println!("Remaining: {}", remaining); // " world"
}
Err(e) => println!("Error: {:?}", e),
}
// map always succeeds if the inner parser succeeds
// The closure cannot cause parser failure
}map transforms successful output; the closure cannot fail.
Basic map_res Usage
use nom::{IResult, bytes::complete::tag, combinator::map_res, error::Error};
fn main() {
// map_res: apply fallible transformation
let input = "hello";
let parser = map_res(tag("hello"), |s: &str| {
if s.len() > 3 {
Ok(s.to_uppercase())
} else {
Err("string too short")
}
});
let result: IResult<&str, String, Error<&str>> = parser(input);
match result {
Ok((remaining, output)) => println!("Success: {}", output),
Err(e) => println!("Error: {:?}", e),
}
// With failing transformation
let input2 = "hi";
let parser2 = map_res(tag("hi"), |_s: &str| -> Result<String, &str> {
Err("transformation failed")
});
let result2 = parser2(input2);
assert!(result2.is_err());
// Transformation failure becomes parser failure
}map_res allows transformation to fail, which becomes a parse error.
Parsing Integers
use nom::{IResult, character::complete::digit1, combinator::map_res, error::Error};
fn parse_number(input: &str) -> IResult<&str, i32, Error<&str>> {
// digit1 parses one or more digits
// map_res converts string to i32, which can fail
map_res(digit1, |s: &str| s.parse::<i32>())(input)
}
fn main() {
// Valid integer
let result = parse_number("12345 rest");
match result {
Ok((remaining, num)) => {
println!("Number: {}", num); // 12345
println!("Remaining: {:?}", remaining); // " rest"
}
Err(e) => println!("Error: {:?}", e),
}
// Overflow fails
let overflow_result = parse_number("999999999999999999999 rest");
assert!(overflow_result.is_err());
// parse() fails for overflow, map_res propagates as parser error
// Not a digit
let invalid_result = parse_number("abc");
assert!(invalid_result.is_err());
// digit1 fails before map_res is called
}map_res is essential for parsing strings to types with FromStr.
When to Use map vs map_res
use nom::{IResult, bytes::complete::take, combinator::{map, map_res}};
// Use map when transformation cannot fail
fn parse_length(input: &[u8]) -> IResult<&[u8], usize> {
// take(4) always succeeds if 4 bytes available
// converting to usize from length calculation cannot fail
map(take(4u8), |bytes: &[u8]| bytes.len())(input)
}
// Use map_res when transformation can fail
fn parse_u32(input: &[u8]) -> IResult<&[u8], u32> {
// Converting bytes to u32 can fail (wrong length, overflow)
map_res(take(4u8), |bytes: &[u8]| {
if bytes.len() == 4 {
Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
} else {
Err("not enough bytes")
}
})(input)
}
fn main() {
let data = &[1, 2, 3, 4, 5, 6, 7, 8];
let (rest, len) = parse_length(data).unwrap();
println!("Length: {}", len);
let (rest2, num) = parse_u32(data).unwrap();
println!("Number: {}", num);
}Choose map for infallible, map_res for fallible transformations.
Backtracking Behavior
use nom::{IResult, bytes::complete::tag, combinator::{map_res, alt}, error::Error};
fn main() {
// map_res enables backtracking on transformation failure
let input = "test";
let parser = alt((
map_res(tag("test"), |_| Err::<i32, &str>("fail")),
map_res(tag("test"), |_| Ok::<i32, &str>(42)),
));
let result: IResult<&str, i32, Error<&str>> = parser(input);
match result {
Ok((_, num)) => println!("Got: {}", num), // 42
Err(_) => println!("All alternatives failed"),
}
// First alternative: parse succeeds, transform fails
// Backtracks to second alternative
// Second alternative: parse succeeds, transform succeeds
// Result: 42
}Transformation failures trigger backtracking, enabling alternative parsing.
Error Handling Differences
use nom::{IResult, bytes::complete::tag, combinator::map_res, error::{Error, ErrorKind}};
fn parse_with_custom_error(input: &str) -> IResult<&str, i32, Error<&str>> {
map_res(
tag("number:"),
|_s: &str| -> Result<i32, Error<&str>> {
// Custom error handling
Err(Error::new(input, ErrorKind::Verify))
}
)(input)
}
fn parse_with_simple_error(input: &str) -> IResult<&str, i32, Error<&str>> {
map_res(
tag("number:"),
|_s: &str| -> Result<i32, &str> {
Err("not a valid number")
}
)(input)
}
fn main() {
let result = parse_with_simple_error("number:123");
match result {
Ok(_) => println!("Success"),
Err(nom::Err::Error(e)) => println!("Error: {:?}", e),
Err(nom::Err::Failure(e)) => println!("Failure: {:?}", e),
Err(nom::Err::Incomplete(_)) => println!("Incomplete"),
}
}map_res allows custom error types for transformation failures.
Validation Use Case
use nom::{IResult, character::complete::digit1, combinator::map_res};
fn parse_port(input: &str) -> IResult<&str, u16, nom::error::Error<&str>> {
// Parse digits, then validate range
map_res(digit1, |s: &str| {
let port: u16 = s.parse().map_err(|_| "invalid number")?;
if port >= 1 && port <= 65535 {
Ok(port)
} else {
Err("port out of range")
}
})(input)
}
fn parse_positive_int(input: &str) -> IResult<&str, i32, nom::error::Error<&str>> {
map_res(digit1, |s: &str| {
let num: i32 = s.parse().map_err(|_| "not a number")?;
if num > 0 {
Ok(num)
} else {
Err("must be positive")
}
})(input)
}
fn main() {
// Valid port
let (rest, port) = parse_port("8080/rest").unwrap();
println!("Port: {}", port); // 8080
// Invalid port (too large)
let result = parse_port("70000");
assert!(result.is_err());
// Negative number rejected by validation
let result = parse_positive_int("0");
assert!(result.is_err());
}map_res is ideal for validation after parsing.
Type Conversion Patterns
use nom::{IResult, bytes::complete::take, combinator::map_res};
// Converting parsed bytes to various types
fn parse_u16_be(input: &[u8]) -> IResult<&[u8], u16> {
map_res(take(2usize), |bytes: &[u8]| {
Ok(u16::from_be_bytes([bytes[0], bytes[1]]))
})(input)
}
fn parse_u32_be(input: &[u8]) -> IResult<&[u8], u32> {
map_res(take(4usize), |bytes: &[u8]| {
if bytes.len() != 4 {
Err("wrong length")
} else {
Ok(u32::from_be_bytes([
bytes[0], bytes[1], bytes[2], bytes[3]
]))
}
})(input)
}
fn main() {
let data: &[u8] = &[0x01, 0x02, 0x03, 0x04, 0x05, 0x06];
let (rest, num16) = parse_u16_be(data).unwrap();
println!("u16: {}", num16); // 258 (0x0102)
let (rest2, num32) = parse_u32_be(data).unwrap();
println!("u32: {}", num32); // 16909060 (0x01020304)
}Binary parsing often requires map_res for type conversions.
Chaining Transformations
use nom::{IResult, character::complete::{digit1, space1, alpha1},
combinator::{map, map_res}, sequence::tuple};
#[derive(Debug)]
struct User {
id: u32,
name: String,
}
fn parse_user(input: &str) -> IResult<&str, User> {
// Chain: parse tuple, then transform
map(
tuple((
map_res(digit1, |s: &str| s.parse::<u32>()),
space1,
alpha1
)),
|(id, _, name)| User {
id,
name: name.to_string(),
}
)(input)
}
fn main() {
let input = "42 Alice";
let (_, user) = parse_user(input).unwrap();
println!("User: {:?}", user); // User { id: 42, name: "Alice" }
// map_res inside tuple for fallible parse
// map outside for infallible struct creation
}Use map_res inside for fallible conversions, map outside for assembly.
Error Types in map_res
use nom::{IResult, bytes::complete::tag, combinator::map_res, error::{Error, ErrorKind}};
// Custom error type
#[derive(Debug)]
enum ParseError {
InvalidFormat,
ConversionError(String),
}
// Using different error types
fn parse_with_custom_error(input: &str) -> IResult<&str, i32, Error<&str>> {
map_res(tag("value:"), |_| Err("my error"))(input)
}
// map_res error can be any type convertible to nom's error
fn parse_flexible(input: &str) -> IResult<&str, i32> {
map_res(
tag("num:"),
|_| Err("conversion failed") // Error type inferred
)(input)
}
fn main() {
let result = parse_flexible("num:");
assert!(result.is_err());
}map_res error types integrate with nom's error handling.
Performance Considerations
use nom::{IResult, character::complete::digit1, combinator::{map, map_res}};
fn main() {
// map: always succeeds after inner parser
// Slightly less overhead than map_res
// map_res: checks Result after transformation
// Additional branch but negligible overhead
// Both are efficient; choose based on semantics, not performance
// Example: string length (infallible)
let parser1 = map(digit1, |s: &str| s.len());
// Example: string to number (fallible)
let parser2 = map_res(digit1, |s: &str| s.parse::<i32>());
// Performance difference is minimal
// Semantic correctness matters more
}Choose based on semantics; performance difference is negligible.
Comparison Summary
use nom::{IResult, bytes::complete::tag, combinator::{map, map_res}};
fn main() {
let input = "hello";
// map: infallible transformation
let parser_map = map(tag("hello"), |s: &str| s.len());
// Type: impl Fn(&str) -> IResult<&str, usize>
// Always succeeds if inner parser succeeds
// Closure returns T (not Result)
// map_res: fallible transformation
let parser_map_res = map_res(tag("hello"), |s: &str| -> Result<usize, &str> {
if s.len() > 3 {
Ok(s.len())
} else {
Err("too short")
}
});
// Type: impl Fn(&str) -> IResult<&str, usize>
// Succeeds only if inner parser AND transformation succeed
// Closure returns Result<T, E>
// Key differences:
// 1. map closure returns T, map_res returns Result<T, E>
// 2. map_res failure triggers backtracking
// 3. map_res integrates with alt and other combinators
// 4. map_res errors participate in error reporting
}Synthesis
Core distinction:
map: Transformation cannot fail; always produces output if parser succeedsmap_res: Transformation can fail; failure becomes parser error
When to use map:
- Type conversions that always succeed (
String,Vec, struct construction) - Computing derived values (length, uppercase, counting)
- Assembling parsed components into structures
- Pure transformations with no validation
When to use map_res:
- String to number parsing (
.parse()) - Validation after parsing (range checks, format validation)
- Type conversions that can fail
- Operations requiring error handling in the transformation
Backtracking integration:
map_resfailures enable alternative parsers inalt- Transformation errors participate in nom's error system
- Enables "parse and validate" as atomic operation
Error handling:
map_reserrors can be custom types- Errors integrate with nom's
ErrorandFailurevariants - Custom error messages improve parser diagnostics
Key insight: The choice between map and map_res is about whether your transformation can failânot about performance. Use map for infallible transformations like building structs or computing lengths. Use map_res when the transformation itself might fail, such as parsing strings to integers or validating constraints. The real power of map_res is that it integrates transformation failures with nom's backtracking system, allowing you to try alternative parsers when validation fails rather than stopping with an error.
