What is the difference between nom::sequence::tuple and separated_pair for parsing structured input?

tuple applies multiple parsers in sequence and returns all results as a tuple, while separated_pair applies two parsers with a separator between them and returns only the two main results as a pair. The key distinction is that tuple captures every parser's result, whereas separated_pair discards the separator's result, returning only the values before and after the separator.

Basic tuple Usage

use nom::{sequence::tuple, character::complete::{char, digit1}, IResult};
 
fn basic_tuple() {
    // tuple applies multiple parsers in sequence, returns all results
    fn parse_coordinates(input: &str) -> IResult<&str, (&str, &str)> {
        tuple((digit1, char(','), digit1))(input)
    }
    
    let (remaining, result) = parse_coordinates("123,456extra").unwrap();
    // remaining = "extra"
    // result = ("123", ',', "456") - a 3-tuple with ALL results
    
    // tuple returns a tuple with one element per parser
    // (Result1, Result2, Result3, ...) matching the input parsers
}

tuple returns a tuple containing every parser's result, including separators.

Basic separated_pair Usage

use nom::{sequence::separated_pair, character::complete::{char, digit1}, IResult};
 
fn basic_separated_pair() {
    // separated_pair applies: first, separator, second
    // Returns only (first_result, second_result), discarding separator
    fn parse_coordinates(input: &str) -> IResult<&str, (&str, &str)> {
        separated_pair(digit1, char(','), digit1)(input)
    }
    
    let (remaining, result) = parse_coordinates("123,456extra").unwrap();
    // remaining = "extra"
    // result = ("123", "456") - a 2-tuple, separator discarded
    
    // separated_pair returns (First, Second), separator is consumed but not returned
}

separated_pair returns only the two main values, discarding the separator.

Return Type Comparison

use nom::{
    sequence::{tuple, separated_pair},
    character::complete::{char, digit1, alpha1},
    IResult,
};
 
fn return_types() {
    // tuple: Returns N-tuple where N = number of parsers
    fn with_tuple(input: &str) -> IResult<&str, (&str, char, &str)> {
        tuple((alpha1, char(':'), digit1))(input)
    }
    
    let (_, result) = with_tuple("key:123").unwrap();
    // result: (&str, char, &str) = ("key", ':', "123")
    // The separator character ':' is included
    
    // separated_pair: Returns 2-tuple, separator discarded
    fn with_separated_pair(input: &str) -> IResult<&str, (&str, &str)> {
        separated_pair(alpha1, char(':'), digit1)(input)
    }
    
    let (_, result) = with_separated_pair("key:123").unwrap();
    // result: (&str, &str) = ("key", "123")
    // The separator character ':' is consumed but not returned
}

tuple includes the separator in the result; separated_pair excludes it.

Parsing Key-Value Pairs

use nom::{
    sequence::separated_pair,
    character::complete::{char, alpha1, digit1},
    bytes::complete::tag,
    IResult,
};
 
fn key_value_parsing() {
    // separated_pair is ideal for key-value pairs
    fn parse_key_value(input: &str) -> IResult<&str, (&str, &str)> {
        separated_pair(
            alpha1,           // key: alphabetic characters
            char('='),        // separator: '='
            digit1            // value: digits
        )(input)
    }
    
    let (_, (key, value)) = parse_key_value("name=42").unwrap();
    assert_eq!(key, "name");
    assert_eq!(value, "42");
    
    // The '=' is consumed but not in the result
    // This is exactly what you want for key-value parsing
    
    // More complex separator
    fn parse_key_value_spaces(input: &str) -> IResult<&str, (&str, &str)> {
        separated_pair(
            alpha1,
            tag(" = "),       // Multi-character separator
            digit1
        )(input)
    }
    
    let (_, (key, value)) = parse_key_value_spaces("name = 42").unwrap();
    assert_eq!(key, "name");
    assert_eq!(value, "42");
    // " = " consumed and discarded
}

separated_pair is the natural choice for key-value structures where the separator is syntax.

Parsing Structured Records

use nom::{
    sequence::{tuple, separated_pair},
    character::complete::{char, digit1, alpha1, space1},
    multi::many1,
    IResult,
};
 
fn record_parsing() {
    // Use tuple when you need ALL components including separators
    fn parse_point_tuple(input: &str) -> IResult<&str, (&str, char, &str)> {
        tuple((
            digit1,
            char(','),
            digit1
        ))(input)
    }
    
    // Use separated_pair when separators are just syntax
    fn parse_point_pair(input: &str) -> IResult<&str, (&str, &str)> {
        separated_pair(
            digit1,
            char(','),
            digit1
        )(input)
    }
    
    // If you need the separator for validation or logging:
    let (_, (x, sep, y)) = parse_point_tuple("10,20").unwrap();
    // You can inspect the separator if needed
    
    // If you only care about the values:
    let (_, (x, y)) = parse_point_pair("10,20").unwrap();
    // Cleaner result type, separator discarded
}

Choose based on whether you need the separator's result.

Complex Separators

use nom::{
    sequence::{separated_pair, tuple},
    character::complete::{char, space0, alpha1, digit1},
    bytes::complete::tag,
    IResult,
};
 
fn complex_separators() {
    // separated_pair with whitespace around separator
    fn parse_loose_key_value(input: &str) -> IResult<&str, (&str, &str)> {
        separated_pair(
            alpha1,
            tuple((space0, char('='), space0)),  // Separator: optional spaces around '='
            digit1
        )(input)
    }
    
    let (_, (key, value)) = parse_loose_key_value("name=42").unwrap();
    let (_, (key, value)) = parse_loose_key_value("name =42").unwrap();
    let (_, (key, value)) = parse_loose_key_value("name= 42").unwrap();
    let (_, (key, value)) = parse_loose_key_value("name = 42").unwrap();
    // All work, separator consumed, only key/value returned
    
    // If you needed to capture the exact separator:
    fn parse_with_separator(input: &str) -> IResult<&str, (&str, (&str, char, &str), &str)> {
        tuple((
            alpha1,
            tuple((space0, char('='), space0)),
            digit1
        ))(input)
    }
    // But this returns a 3-tuple with separator details
}

separated_pair discards complex separators just like simple ones.

Multiple Values with tuple

use nom::{
    sequence::tuple,
    character::complete::{char, digit1},
    IResult,
};
 
fn multiple_values() {
    // tuple can have any number of parsers
    fn parse_three_coords(input: &str) -> IResult<&str, (&str, char, &str, char, &str)> {
        tuple((
            digit1,     // x
            char(','),  // separator
            digit1,     // y
            char(','),  // separator
            digit1      // z
        ))(input)
    }
    
    let (_, (x, sep1, y, sep2, z)) = parse_three_coords("1,2,3").unwrap();
    // Returns all 5 results
    
    // separated_pair only works for exactly 2 values with 1 separator
    // For 3+ values, you'd need to nest or use tuple
    
    // Alternative: combine separated_pair with map
    fn parse_two_coords(input: &str) -> IResult<&str, ((&str, &str), &str)> {
        tuple((
            separated_pair(digit1, char(','), digit1),
            char(','),
            digit1
        ))(input)
    }
}

tuple scales to any number of parsers; separated_pair is fixed at 2 values.

Nested Structures

use nom::{
    sequence::{tuple, separated_pair, delimited},
    character::complete::{char, digit1, alpha1},
    bytes::complete::tag,
    IResult,
};
 
fn nested_structures() {
    // Nested separated_pair for nested structures
    fn parse_nested_pair(input: &str) -> IResult<&str, (&str, (&str, &str))> {
        separated_pair(
            alpha1,
            char(':'),
            delimited(char('('), separated_pair(alpha1, char(','), alpha1), char(')'))
        )(input)
    }
    
    let (_, (outer_key, (inner_a, inner_b))) = parse_nested_pair("key:(a,b)").unwrap();
    // outer_key = "key"
    // inner_a = "a", inner_b = "b"
    // All separators (':', '(', ',', ')') consumed and discarded
    
    // Equivalent with tuple for comparison
    fn parse_nested_tuple(input: &str) -> IResult<&str, (&str, char, char, &str, char, &str, char)> {
        tuple((
            alpha1, char(':'), char('('),
            alpha1, char(','), alpha1,
            char(')')
        ))(input)
    }
    // Much more complex result type!
}

separated_pair produces cleaner result types when separators are not needed.

When to Use Each

use nom::{
    sequence::{tuple, separated_pair},
    character::complete::{char, digit1, alpha1},
    IResult,
};
 
fn choosing_between_them() {
    // Use separated_pair when:
    // 1. Parsing two values with a separator between them
    // 2. You don't need the separator's result
    // 3. The separator is purely structural syntax
    
    // Examples: key=value, item1,item2, before|after
    
    fn key_value(input: &str) -> IResult<&str, (&str, &str)> {
        separated_pair(alpha1, char('='), alpha1)(input)
    }
    
    // Use tuple when:
    // 1. You have more than 2 values
    // 2. You need the separator's result
    // 3. Multiple different separators
    
    // Examples: "a,b,c" where you need the separators
    //           complex parsing with intermediate values needed
    
    fn with_separator_info(input: &str) -> IResult<&str, (&str, char, &str)> {
        tuple((alpha1, char('='), alpha1))(input)
    }
    // Use this if you need to know which separator was used
    // (e.g., parsing both '=' and ':' as separators and needing to distinguish)
}

Use separated_pair for key-value patterns; use tuple for complex or multi-value cases.

Error Handling

use nom::{
    sequence::{tuple, separated_pair},
    character::complete::{char, digit1, alpha1},
    error::{ErrorKind, VerboseError, VerboseErrorKind},
    IResult,
};
 
fn error_handling() {
    // Both propagate errors similarly, but tuple provides more context
    
    fn parse_tuple(input: &str) -> IResult<&str, (&str, char, &str)> {
        tuple((alpha1, char(':'), digit1))(input)
    }
    
    fn parse_separated(input: &str) -> IResult<&str, (&str, &str)> {
        separated_pair(alpha1, char(':'), digit1)(input)
    }
    
    // Error on first parser failure
    let result = parse_tuple("123:value");
    // Fails: alpha1 expects letters, got "123"
    
    // Error on separator failure
    let result = parse_tuple("key=value");
    // Fails: char(':') expects ':', got '='
    
    // Error on second value failure
    let result = parse_tuple("key:abc");
    // Fails: digit1 expects digits, got "abc"
    
    // Same error behavior for separated_pair
    let result = parse_separated("key:abc");
    // Fails: digit1 expects digits, got "abc"
}

Both combinators fail at the first parser that fails; error messages are similar.

Combining with Other Combinators

use nom::{
    sequence::{tuple, separated_pair, preceded, delimited},
    character::complete::{char, digit1, alpha1, space0},
    bytes::complete::tag,
    combinator::map,
    IResult,
};
 
fn combined_combinators() {
    // separated_pair inside delimited
    fn parse_bracketed_kv(input: &str) -> IResult<&str, (&str, &str)> {
        delimited(
            char('['),
            separated_pair(alpha1, char('='), digit1),
            char(']')
        )(input)
    }
    
    let (_, (key, value)) = parse_bracketed_kv("[name=42]").unwrap();
    assert_eq!(key, "name");
    assert_eq!(value, "42");
    // Brackets consumed and discarded
    
    // separated_pair with map for custom types
    #[derive(Debug, PartialEq)]
    struct KeyValue<'a> {
        key: &'a str,
        value: &'a str,
    }
    
    fn parse_kv_struct(input: &str) -> IResult<&str, KeyValue> {
        map(
            separated_pair(alpha1, char('='), digit1),
            |(key, value)| KeyValue { key, value }
        )(input)
    }
    
    let (_, kv) = parse_kv_struct("count=123").unwrap();
    assert_eq!(kv, KeyValue { key: "count", value: "123" });
    
    // tuple for more complex mapping
    fn parse_point_struct(input: &str) -> IResult<&str, (i32, i32)> {
        map(
            tuple((digit1, char(','), digit1)),
            |(x, _, y)| (x.parse().unwrap(), y.parse().unwrap())
        )(input)
    }
    
    // Note: map can drop separator in tuple too, but separated_pair is clearer
}

Both work well with other combinators; separated_pair often produces cleaner intermediate types.

Performance Characteristics

use nom::{
    sequence::{tuple, separated_pair},
    character::complete::{char, digit1},
    IResult,
};
 
fn performance() {
    // Both have similar performance - they're pure parser combinators
    
    // separated_pair: applies 3 parsers, returns 2 results
    fn fast_parse(input: &str) -> IResult<&str, (&str, &str)> {
        separated_pair(digit1, char(','), digit1)(input)
    }
    
    // tuple: applies 3 parsers, returns 3 results
    fn with_separator(input: &str) -> IResult<&str, (&str, char, &str)> {
        tuple((digit1, char(','), digit1))(input)
    }
    
    // Performance difference is negligible - both are linear in input size
    
    // The main difference is in result allocation:
    // separated_pair returns 2-tuple
    // tuple returns N-tuple where N is number of parsers
    
    // For hot paths, choose based on result type convenience
}

Performance is similar; choose based on result type convenience.

Practical CSV Parsing Example

use nom::{
    sequence::{separated_pair, tuple},
    character::complete::{char, digit1, alpha1},
    multi::separated_list1,
    IResult,
};
 
fn csv_example() {
    // Parsing "name,age,score" format
    // With separated_pair, we need to nest for multiple separators
    
    // Option 1: Use tuple for fixed number of fields
    fn parse_record_tuple(input: &str) -> IResult<&str, (&str, char, &str, char, &str)> {
        tuple((alpha1, char(','), digit1, char(','), digit1))(input)
    }
    
    let (_, (name, _, age, _, score)) = parse_record_tuple("Alice,25,100").unwrap();
    // name = "Alice", age = "25", score = "100"
    // Separators are in result
    
    // Option 2: Use separated_list1 for variable fields
    fn parse_record_list(input: &str) -> IResult<&str, Vec<&str>> {
        separated_list1(char(','), alpha1)(input)  // Or a more complex field parser
    )(input)
    // This is different - parses a list, not separated_pair
    
    // Option 3: Combine separated_pair with map for cleaner results
    fn parse_record_clean(input: &str) -> IResult<&str, (&str, &str, &str)> {
        let (remaining, first) = alpha1(input)?;
        let (remaining, _) = char(',')(remaining)?;
        let (remaining, second) = digit1(remaining)?;
        let (remaining, _) = char(',')(remaining)?;
        let (remaining, third) = digit1(remaining)?;
        Ok((remaining, (first, second, third)))
    }
    // Or use tuple and map to drop separators
}

For multiple values, tuple is more convenient; separated_pair excels for two-value cases.

Synthesis

Quick comparison:

Aspect tuple separated_pair
Number of parsers Any number (1, 2, 3, ...) Exactly 3 (first, sep, second)
Return type N-tuple with all results 2-tuple (first, second)
Separator in result Yes No
Typical use case Multiple values, need all parts Key-value pairs
Composable with map Yes Yes

When to use separated_pair:

use nom::{sequence::separated_pair, character::complete::char, IResult};
 
fn use_separated_pair() {
    // 1. Key-value pairs
    fn key_value(input: &str) -> IResult<&str, (&str, &str)> {
        separated_pair(key_parser, char('='), value_parser)(input)
    }
    
    // 2. Two-part structures with syntactic separator
    fn range(input: &str) -> IResult<&str, (&str, &str)> {
        separated_pair(number, char('-'), number)(input)
    }
    
    // 3. Before/after patterns
    fn split_at(input: &str) -> IResult<&str, (&str, &str)> {
        separated_pair(content, tag("::"), suffix)(input)
    }
}

When to use tuple:

use nom::{sequence::tuple, character::complete::char, IResult};
 
fn use_tuple() {
    // 1. More than 2 values
    fn three_parts(input: &str) -> IResult<&str, (&str, &str, &str)> {
        tuple((parser1, parser2, parser3))(input)
    }
    
    // 2. Need separator result
    fn with_sep_info(input: &str) -> IResult<&str, (&str, char, &str)> {
        tuple((first, separator, second))(input)
    }
    
    // 3. Variable separators (need to know which one matched)
    fn flexible_sep(input: &str) -> IResult<&str, (&str, char, &str)> {
        tuple((alpha, alt((char('='), char(':'))), alpha))(input)
    }
}

Key insight: separated_pair and tuple serve different but overlapping purposes. separated_pair is specialized for the common "value-separator-value" pattern where the separator is purely syntactic and its result is discarded. This produces a cleaner 2-tuple result type. tuple is general-purpose, capturing all parser results in an N-tuple, which includes separators. Use separated_pair when parsing key-value pairs, ranges, or any two-part structure separated by syntactic markup—you'll get a simpler result type. Use tuple when you need the separator's result (perhaps multiple possible separators), when you have more than two values to parse, or when you need all parser results for further processing. The choice often comes down to the result type you want: separated_pair gives (First, Second), while tuple((First, Sep, Second)) gives (First, Sep, Second). If you'd immediately map to discard the separator, separated_pair does that for you.