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.
