How does nom::sequence::tuple combine multiple parsers into a single parser returning a tuple of results?

nom::sequence::tuple takes multiple parsers and combines them into a single parser that applies each one in sequence, returning their results as a tuple. If any parser fails, the entire tuple parser fails immediately with the first error, providing short-circuit behavior for parsing sequences.

Basic Tuple Parser Usage

use nom::{
    sequence::tuple,
    character::complete::{alpha1, digit1, space1},
    IResult,
};
 
fn parse_user() -> IResult<&str, (&str, &str, &str)> {
    // Apply three parsers in sequence
    tuple((alpha1, space1, digit1))(input)
}
 
fn main() {
    let input = "john 42";
    let result = tuple((alpha1, space1, digit1))(input);
    
    // Result: Ok(("", ("john", " ", "42")))
    // Input consumed: all
    // Output: tuple of each parser's result
}

tuple applies parsers left-to-right, collecting results into a tuple.

Type Signature and Return Value

use nom::sequence::tuple;
use nom::IResult;
 
// tuple takes a tuple of parsers and returns a parser producing a tuple of results
// Signature (simplified):
// fn tuple<I, O1, O2, ..., E, P1, P2, ...>(parsers: (P1, P2, ...)) 
//     -> impl Fn(I) -> IResult<I, (O1, O2, ...)>
// where
//     P1: Fn(I) -> IResult<I, O1, E>,
//     P2: Fn(I) -> IResult<I, O2, E>,
//     ...
 
fn type_demonstration() {
    let parser = tuple((
        |i: &str| Ok((i, 1i32)),      // Parser 1 returns i32
        |i: &str| Ok((i, "hello")),   // Parser 2 returns &str
        |i: &str| Ok((i, 2.0f64)),    // Parser 3 returns f64
    ));
    
    // Result type: (&str, (i32, &str, f64))
    // First element is remaining input
    // Second element is tuple of all parser results
}

The output tuple type matches the input parser tuple structure.

Short-Circuit Error Behavior

use nom::{
    sequence::tuple,
    character::complete::{alpha1, digit1},
    error::{Error, ErrorKind},
    IResult,
};
 
fn error_handling() {
    // If any parser fails, the entire tuple fails immediately
    let result = tuple((
        alpha1,  // Succeeds: "hello"
        digit1,  // Fails: no digits after "hello world"
    ))("hello world");
    
    // Result: Err(Error { input: " world", code: Digit })
    // Parser stops at first failure, doesn't continue
    
    // Compare with applying parsers separately:
    // Each parser would process remaining input
}

Failures short-circuit; remaining parsers aren't called after an error.

Combining Different Parser Types

use nom::{
    sequence::tuple,
    character::complete::{alpha1, digit1, char, space1},
    bytes::complete::tag,
    number::complete::u32,
    IResult,
};
 
fn mixed_parsers(input: &str) -> IResult<&str, (&str, char, u32, &str)> {
    tuple((
        alpha1,           // Parse alphabetic characters
        char(' '),        // Parse specific character
        u32,              // Parse unsigned integer
        tag!("end"),      // Parse literal string
    ))(input)
}
 
fn main() {
    let result = mixed_parsers("abc 123end");
    // Ok(("", ("abc", ' ', 123, "end")))
}

tuple works with any parsers that share the same input and error types.

Tuple Sizes and Variadic Behavior

use nom::sequence::tuple;
use nom::character::complete::anychar;
 
fn tuple_sizes() {
    // 2-tuple
    let _: IResult<&str, (char, char)> = tuple((anychar, anychar))("ab");
    
    // 3-tuple
    let _: IResult<&str, (char, char, char)> = tuple((anychar, anychar, anychar))("abc");
    
    // 4-tuple
    let _: IResult<&str, (char, char, char, char)> = 
        tuple((anychar, anychar, anychar, anychar))("abcd");
    
    // nom supports tuples up to 21 elements
    // For more parsers, use nested tuples or custom structs
}

tuple supports various sizes; nom implements for tuples from 0 to 21 elements.

Working with the Results

use nom::{
    sequence::tuple,
    character::complete::{alpha1, digit1},
    combinator::map,
    IResult,
};
 
fn process_results(input: &str) -> IResult<&str, String> {
    // Use map to transform tuple results
    let (remaining, (name, id)) = tuple((alpha1, digit1))(input)?;
    
    Ok((remaining, format!("{}-{}", name, id)))
}
 
fn map_pattern(input: &str) -> IResult<&str, (String, u32)> {
    // Destructure in map closure
    map(
        tuple((alpha1, digit1)),
        |(name, id): (&str, &str)| (name.to_uppercase(), id.parse().unwrap())
    )(input)
}

Pattern matching in closures makes tuple results easy to work with.

Comparison with Other Sequence Combinators

use nom::{
    sequence::{tuple, preceded, delimited, pair, separated_pair},
    character::complete::{alpha1, digit1, char},
    IResult,
};
 
fn sequence_comparison() {
    let input = "abc123";
    
    // tuple: Apply all, return all results
    let _: IResult<&str, (&str, &str)> = tuple((alpha1, digit1))(input);
    
    // pair: Apply two, return both (special case of tuple)
    let _: IResult<&str, (&str, &str)> = pair(alpha1, digit1)(input);
    
    // preceded: Apply two, return second
    let _: IResult<&str, &str> = preceded(alpha1, digit1)(input);
    
    // delimited: Apply three, return middle
    let input2 = "(abc)";
    let _: IResult<&str, &str> = delimited(char('('), alpha1, char(')'))(input2);
    
    // separated_pair: Apply three, return first and third
    let input3 = "abc,123";
    let _: IResult<&str, (&str, &str)> = separated_pair(alpha1, char(','), digit1)(input3);
}

tuple returns all results; other combinators may discard some.

Nested Tuples

use nom::{
    sequence::tuple,
    character::complete::{alpha1, digit1, space1, char},
    IResult,
};
 
fn nested_tuples(input: &str) -> IResult<&str, ((&str, &str), &str)> {
    // Nest tuples for hierarchical grouping
    tuple((
        tuple((alpha1, digit1)),  // Inner tuple: ("name", "1")
        space1,
    ))(input)
}
 
fn main() {
    let result = nested_tuples("abc123 ");
    // Ok(("", (("abc", "123"), " ")))
}

Nested tuples create hierarchical result structures.

Building Complex Parsers

use nom::{
    sequence::tuple,
    character::complete::{alpha1, digit1, char, space0, space1},
    bytes::complete::tag,
    combinator::map,
    IResult,
};
 
#[derive(Debug)]
struct HttpRequest {
    method: String,
    path: String,
    version: String,
}
 
fn parse_http_request(input: &str) -> IResult<&str, HttpRequest> {
    map(
        tuple((
            alpha1,      // Method
            space1,      // Space
            alpha1,      // Path (simplified)
            space1,      // Space
            tag!("HTTP/1.1"),  // Version
        )),
        |(method, _, path, _, version)| HttpRequest {
            method: method.to_string(),
            path: path.to_string(),
            version: version.to_string(),
        }
    )(input)
}
 
fn main() {
    let result = parse_http_request("GET index HTTP/1.1");
    // Ok(("", HttpRequest { method: "GET", path: "index", version: "HTTP/1.1" }))
}

Complex structures are built by combining tuples with map.

Zero-Element Tuple

use nom::sequence::tuple;
 
fn empty_tuple(input: &str) -> IResult<&str, ()> {
    // Empty tuple always succeeds, returns unit
    tuple(())(input)
}
 
fn main() {
    let result = tuple(())("any input");
    // Ok(("any input", ()))
    // No parsing done, returns unit
}

An empty tuple is a no-op parser that always succeeds.

Single-Element Tuple

use nom::{sequence::tuple, character::complete::alpha1, IResult};
 
fn single_element(input: &str) -> IResult<&str, (&str,)> {
    // Single-element tuple returns single-element tuple
    tuple((alpha1,))(input)
}
 
fn main() {
    let result = tuple((alpha1,))("hello");
    // Ok(("", ("hello",)))
    // Note: result is a 1-tuple, not just &str
}

Single-element tuples still return tuples; use the parser directly for single values.

Optional Parsers in Tuples

use nom::{
    sequence::tuple,
    character::complete::{alpha1, space0},
    combinator::opt,
    IResult,
};
 
fn with_optional(input: &str) -> IResult<&str, (&str, Option<&str>)> {
    tuple((
        alpha1,
        opt(alpha1),  // Optional second word
    ))(input)
}
 
fn main() {
    let result1 = with_optional("hello");
    // Ok(("", ("hello", None)))
    
    let result2 = with_optional("helloworld");
    // Ok(("", ("helloworld", None)))  // alpha1 consumes all
}

Combine tuple with opt for optional components.

Many Parsers in Tuples

use nom::{
    sequence::tuple,
    character::complete::char,
    multi::many0,
    combinator::map,
    IResult,
};
 
fn with_repetition(input: &str) -> IResult<&str, (char, Vec<char>)> {
    tuple((
        char('a'),
        many0(char('b')),  // Collect multiple 'b's
    ))(input)
}
 
fn main() {
    let result = with_repetition("abbb");
    // Ok(("", ('a', vec
!['b', 'b', 'b'])))
}

Tuples can include repetition combinators like many0 or many1.

Error Type Propagation

use nom::{
    sequence::tuple,
    character::complete::digit1,
    error::{Error, ParseError},
    IResult,
};
 
fn error_propagation(input: &str) -> IResult<&str, (&str, &str)> {
    // All parsers must have compatible error types
    tuple((
        |i: &str| -> IResult<&str, &str> { Ok((i, "prefix")) },
        digit1,
    ))(input)
}
 
// Custom error types require all parsers to use compatible errors
fn custom_errors<I, E: ParseError<I>>(input: I) -> IResult<I, (&str, &str), E> {
    tuple((
        |i: I| Ok((i, "a")),
        |i: I| Ok((i, "b")),
    ))(input)
}

All parsers in a tuple must use compatible error types.

Backtracking Behavior

use nom::{
    sequence::tuple,
    character::complete::{alpha1, digit1},
    IResult,
};
 
fn backtracking(input: &str) {
    // tuple does NOT backtrack on success
    // If first parser succeeds but second fails, input is partially consumed
    
    let result = tuple((
        alpha1,  // Succeeds, consumes "hello"
        digit1,  // Fails on " world"
    ))("hello world");
    
    // Result: Err(Error { input: " world", code: Digit })
    // Input "hello" was consumed by first parser
    
    // For backtracking, use nom::combinator::consumed or other patterns
}

Successful parsers consume input; failed subsequent parsers don't restore it.

Complete Parsing Example

use nom::{
    sequence::tuple,
    character::complete::{alpha1, digit1, char, space0, space1, multispace0},
    bytes::complete::tag,
    combinator::map,
    multi::separated_list0,
    IResult,
};
 
#[derive(Debug, PartialEq)]
struct Config {
    name: String,
    value: u32,
    items: Vec<String>,
}
 
fn parse_config(input: &str) -> IResult<&str, Config> {
    map(
        tuple((
            alpha1,                           // name
            space1,                           // separator
            digit1,                           // value (as string)
            space0,                           // optional space
            char(':'),                        // colon
            space0,                           // optional space
            separated_list0(char(','), alpha1) // items
        )),
        |(name, _, value, _, _, _, items)| Config {
            name: name.to_string(),
            value: value.parse().unwrap(),
            items: items.into_iter().map(|s| s.to_string()).collect(),
        }
    )(input)
}
 
fn main() {
    let input = "config 42: alpha,beta,gamma";
    let result = parse_config(input);
    
    assert_eq!(result, Ok((
        "",
        Config {
            name: "config".to_string(),
            value: 42,
            items: vec
!["alpha".to_string(), "beta".to_string(), "gamma".to_string()],
        }
    )));
}

Comparison with Do-Notation Style

use nom::{
    sequence::tuple,
    character::complete::{alpha1, digit1},
    IResult,
};
 
// Using tuple combinator
fn with_tuple(input: &str) -> IResult<&str, (&str, &str)> {
    tuple((alpha1, digit1))(input)
}
 
// Using explicit sequencing (similar effect)
fn with_explicit(input: &str) -> IResult<&str, (&str, &str)> {
    let (input, first) = alpha1(input)?;
    let (input, second) = digit1(input)?;
    Ok((input, (first, second)))
}
 
// Both approaches are equivalent
// tuple is more concise for simple sequences
// Explicit sequencing allows more complex logic between parsers

tuple is syntactic sugar for sequential parser application.

Practical Pattern: Parsing Records

use nom::{
    sequence::tuple,
    character::complete::{alpha1, digit1, char, space0, line_ending},
    bytes::complete::take_until,
    combinator::map,
    multi::many1,
    IResult,
};
 
#[derive(Debug)]
struct Record {
    id: u32,
    name: String,
    description: String,
}
 
fn parse_record(input: &str) -> IResult<&str, Record> {
    map(
        tuple((
            digit1,
            char('\t'),
            alpha1,
            char('\t'),
            take_until("\n"),
            line_ending,
        )),
        |(id, _, name, _, desc, _)| Record {
            id: id.parse().unwrap(),
            name: name.to_string(),
            description: desc.to_string(),
        }
    )(input)
}
 
fn parse_records(input: &str) -> IResult<&str, Vec<Record>> {
    many1(parse_record)(input)
}
 
fn main() {
    let input = "1\talice\tfirst user\n2\tbob\tsecond user\n";
    let records = parse_records(input);
    // Parses both records into a Vec<Record>
}

Summary Table

fn summary_table() {
    // ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
    // │ Combinator          │ Input Parsers │ Result Type         │ Behavior   │
    // ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
    // │ tuple((A, B, C))    │ 3 parsers     │ (OA, OB, OC)        │ Keep all   │
    // │ pair(A, B)          │ 2 parsers      │ (OA, OB)            │ Keep all   │
    // │ preceded(A, B)      │ 2 parsers      │ OB                  │ Discard A │
    // │ delimited(A, B, C)  │ 3 parsers      │ OB                   │ Discard A,C│
    // │ separated_pair(A,B,C)│ 3 parsers     │ (OA, OC)            │ Discard B │
    // │ (tuple(()))         │ 0 parsers      │ ()                   │ No-op     │
    // │ (tuple((A,)))       │ 1 parser       │ (OA,)                │ 1-tuple   │
    // ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
}

Summary

fn summary() {
    // ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
    // │ Aspect              │ Behavior                                   │
    // ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
    // │ Ordering            │ Parsers applied left-to-right             │
    // │ Result type         │ Tuple of individual parser results        │
    // │ Error handling      │ Short-circuits on first failure           │
    // │ Input consumption   │ Sequential, no backtracking on success    │
    // │ Maximum size        │ Up to 21 parsers in one tuple            │
    // │ Empty tuple          │ Always succeeds, returns ()               │
    // │ Nesting             │ Supports nested tuples                    │
    // │ Error types         │ All parsers must share error type         │
    // ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
    
    // Key points:
    // 1. tuple applies multiple parsers in sequence
    // 2. Results are collected into a tuple matching parser order
    // 3. Short-circuits on first error, returns that error
    // 4. Supports 0-21 parsers directly
    // 5. Works with any parsers sharing input and error types
    // 6. Use map to transform tuple into structured data
    // 7. Nested tuples create hierarchical structures
    // 8. Combine with opt, many0, etc. for optional/repeated elements
    // 9. pair is a special case of tuple for two parsers
    // 10. More concise than explicit sequencing for simple cases
}

Key insight: nom::sequence::tuple is the foundational combinator for sequencing multiple parsers in nom. It represents the concept of "apply these parsers in order and collect all results." Unlike preceded or delimited which discard certain results, tuple preserves every parser's output in the result tuple. The key behaviors are: (1) left-to-right application, (2) immediate short-circuit on failure with no backtracking of consumed input, and (3) result collection into a tuple that mirrors the parser structure. Use tuple when you need all parsed values; use specialized combinators like preceded when you want to discard prefix values. For complex structures, combine tuple with map to transform the tuple into domain types. The 21-element limit covers most practical cases; for more complex sequences, use nested tuples or custom parser functions with explicit sequencing.