How does nom::sequence::tuple combine multiple parsers into a single tuple result?

nom::sequence::tuple takes multiple parsers as arguments and returns a parser that applies each one in sequence, collecting all successful results into a tuple with the same arity as the input parsers—if any parser fails, the entire tuple parser fails and backtracks. The function tuple((parser1, parser2, parser3)) produces a parser that returns ((I, O1, O2, O3), I) on success, where I is the remaining input and O1, O2, O3 are the outputs of each sub-parser. This enables building complex parsers from simpler ones while preserving all parsed values in a structured result.

Basic tuple Usage

use nom::{sequence::tuple, character::complete::{digit1, alpha1}, IResult};
 
fn basic_tuple() {
    // Combine two parsers
    let mut parser = tuple((alpha1, digit1));
    
    // Parse "abc123"
    let result: IResult<&str, (&str, &str)> = parser("abc123");
    
    match result {
        Ok((remaining, (letters, numbers))) => {
            assert_eq!(remaining, "");
            assert_eq!(letters, "abc");
            assert_eq!(numbers, "123");
        }
        Err(e) => panic!("Parse failed: {:?}", e),
    }
}

tuple returns a tuple of all parser results in order.

Three or More Parsers

use nom::{sequence::tuple, character::complete::{digit1, alpha1, char}, IResult};
 
fn multiple_parsers() {
    // Combine three parsers
    let mut parser = tuple((alpha1, char(':'), digit1));
    
    let result: IResult<&str, (&str, char, &str)> = parser("key:42");
    
    match result {
        Ok((remaining, (key, colon, value))) => {
            assert_eq!(remaining, "");
            assert_eq!(key, "key");
            assert_eq!(colon, ':');
            assert_eq!(value, "42");
        }
        Err(e) => panic!("Parse failed: {:?}", e),
    }
}

The output tuple grows with each additional parser.

Type Signature

use nom::{IResult, InputLength, Parser};
 
// The tuple function signature (simplified):
// pub fn tuple<I, O>(mut tuple: (P1, P2, ...)) -> impl Fn(I) -> IResult<I, (O1, O2, ...)>
// where
//     P1: Parser<I, O1>,
//     P2: Parser<I, O2>,
//     ...
 
// The input is a tuple of parsers
// The output is a parser that produces a tuple of outputs
 
fn type_signature() {
    // Input: (Parser<I, O1>, Parser<I, O2>, Parser<I, O3>)
    // Output: impl Parser<I, (O1, O2, O3)>
    
    // The input and output tuple arities match
    // Each parser in the input contributes one element to the output
}

The output tuple has the same number of elements as the input parser tuple.

Failure Propagation

use nom::{sequence::tuple, character::complete::{digit1, alpha1}, error::ErrorKind, IResult};
 
fn failure_propagation() {
    let mut parser = tuple((alpha1, digit1));
    
    // First parser fails
    let result = parser("123abc");
    assert!(result.is_err());
    // The entire tuple fails; digit1 is never called
    
    // Second parser fails
    let result = parser("abcdef");
    assert!(result.is_err());
    // alpha1 succeeds ("abcdef"), then digit1 fails
    
    // Partial consumption behavior:
    // On failure, the input is NOT consumed
    // The parser backtracks to the original position
}

If any parser fails, the entire tuple fails without consuming input.

Sequential Application

use nom::{sequence::tuple, character::complete::{alpha1, digit1, multispace0}, IResult};
 
fn sequential_application() {
    let mut parser = tuple((alpha1, multispace0, digit1));
    
    // Each parser is applied to the remaining input from the previous
    let result = parser("word   123extra");
    
    match result {
        Ok((remaining, (word, spaces, number))) => {
            // word is parsed first from full input
            assert_eq!(word, "word");
            // spaces is parsed from "   123extra"
            assert_eq!(spaces, "   ");
            // number is parsed from "123extra"
            assert_eq!(number, "123");
            // remaining is what's left
            assert_eq!(remaining, "extra");
        }
        Err(_) => panic!("Should succeed"),
    }
}

Each parser sees the input remaining after the previous parser.

Combining with Other Combinators

use nom::{sequence::tuple, character::complete::{char, digit1}, combinator::map, IResult};
 
fn with_map() {
    // Transform tuple output into a struct
    #[derive(Debug, PartialEq)]
    struct Point {
        x: i32,
        y: i32,
    }
    
    let mut parser = map(
        tuple((char('('), digit1, char(','), digit1, char(')'))),
        |(_, x, _, y, _): (char, &str, char, &str, char)| Point {
            x: x.parse().unwrap(),
            y: y.parse().unwrap(),
        }
    );
    
    let result = parser("(3,4)");
    assert_eq!(result, Ok(("", Point { x: 3, y: 4 })));
}

Combine tuple with map to transform results into structured types.

Ignoring Values in Tuple

use nom::{sequence::tuple, character::complete::{char, digit1}, combinator::map, IResult};
 
fn ignoring_values() {
    // When some parsed values are not needed
    let mut parser = map(
        tuple((char('('), digit1, char(','), digit1, char(')'))),
        |(_, x, _, y, _)| (x, y)  // Keep only the digits
    );
    
    let result: IResult<&str, (&str, &str)> = parser("(10,20)");
    assert_eq!(result, Ok(("", ("10", "20"))));
}
 
// Alternative: use preceded, delimited, separated_pair
use nom::sequence::{preceded, delimited, separated_pair};
 
fn alternatives() {
    // separated_pair is more idiomatic for this pattern
    let mut parser = separated_pair(digit1, char(','), digit1);
    let result = parser("10,20");
    assert_eq!(result, Ok(("", ("10", "20"))));
    
    // delimited for wrapped content
    let mut parser = delimited(char('('), separated_pair(digit1, char(','), digit1), char(')'));
    let result = parser("(10,20)");
    assert_eq!(result, Ok(("", ("10", "20"))));
}

For common patterns, specialized combinators may be more readable than tuple.

Nested Tuple Parsers

use nom::{sequence::tuple, character::complete::{alpha1, digit1, char}, IResult};
 
fn nested_tuples() {
    // Tuple parsers can be nested
    let mut parser = tuple((
        alpha1,
        tuple((char(':'), digit1)),
        alpha1
    ));
    
    let result: IResult<&str, (&str, (char, &str), &str)> = parser("a:123b");
    
    match result {
        Ok((remaining, (first, (_colon, number), last))) => {
            assert_eq!(first, "a");
            assert_eq!(number, "123");
            assert_eq!(last, "b");
            assert_eq!(remaining, "");
        }
        Err(_) => panic!("Should succeed"),
    }
}

Tuples can contain other tuples, creating nested result structures.

Zero and One Element Tuples

use nom::{sequence::tuple, character::complete::alpha1, IResult};
 
fn zero_one_element() {
    // Zero-element tuple (unit type)
    let mut empty_parser = tuple(());
    let result: IResult<&str, ()> = empty_parser("anything");
    assert_eq!(result, Ok(("anything", ())));
    // Always succeeds, consumes nothing
    
    // One-element tuple (wraps in single-element tuple)
    let mut single_parser = tuple((alpha1,));
    let result: IResult<&str, (&str,)> = single_parser("abc");
    assert_eq!(result, Ok(("", ("abc",))));
    // Note the trailing comma for single-element tuple syntax
}

Empty tuples always succeed; single-element tuples wrap the result.

Working with Different Return Types

use nom::{
    sequence::tuple,
    character::complete::{char, digit1, alpha1},
    bytes::complete::tag,
    IResult,
};
 
fn different_types() {
    // Each parser can return a different type
    let mut parser = tuple((
        alpha1,        // returns &str
        char(':'),     // returns char
        digit1,        // returns &str
        tag("end"),    // returns &str
    ));
    
    let result: IResult<&str, (&str, char, &str, &str)> = parser("key:123end");
    
    match result {
        Ok((remaining, (alpha, colon, digits, tag_result))) => {
            assert_eq!(alpha, "key");
            assert_eq!(colon, ':');
            assert_eq!(digits, "123");
            assert_eq!(tag_result, "end");
            assert_eq!(remaining, "");
        }
        Err(_) => panic!("Should succeed"),
    }
}

Each parser in the tuple contributes its own return type to the output tuple.

Parsing Structured Data

use nom::{
    sequence::tuple,
    character::complete::{char, digit1, multispace0},
    combinator::map,
    IResult,
};
 
fn structured_data() {
    // Parse a key-value pair with type annotation
    // Format: "name : type = value"
    
    #[derive(Debug, PartialEq)]
    struct Field {
        name: String,
        type_name: String,
        value: String,
    }
    
    let mut parser = map(
        tuple((
            alpha1,                    // name
            multispace0,
            char(':'),
            multispace0,
            alpha1,                    // type
            multispace0,
            char('='),
            multispace0,
            digit1,                    // value
        )),
        |(name, _, _, _, type_name, _, _, _, value)| Field {
            name: name.to_string(),
            type_name: type_name.to_string(),
            value: value.to_string(),
        }
    );
    
    let result = parser("count : int = 42");
    assert_eq!(result, Ok(("", Field {
        name: "count".to_string(),
        type_name: "int".to_string(),
        value: "42".to_string(),
    })));
}

Complex structured parsing uses tuple to sequence all components.

Error Handling

use nom::{sequence::tuple, character::complete::{alpha1, digit1}, error::{Error, ErrorKind}, IResult};
 
fn error_handling() {
    let mut parser = tuple((alpha1, digit1));
    
    // Error on first parser
    let result = parser("123");
    match result {
        Err(nom::Err::Error(Error { input, code })) => {
            // input is the original input
            // code indicates what failed
            assert_eq!(input, "123");
            // alpha1 failed
        }
        _ => panic!("Should error"),
    }
    
    // Error on second parser
    let result = parser("abc");
    match result {
        Err(nom::Err::Error(Error { input, code })) => {
            // input is what digit1 saw ("")
            // because alpha1 consumed "abc"
            // But nom backtracks, so original input is restored
            // (depending on error mode)
        }
        _ => panic!("Should error"),
    }
}

Errors indicate which parser failed; input position is restored on failure.

Complete vs Streaming

use nom::{
    sequence::tuple,
    character::complete::digit1,
    character::streaming::digit1 as streaming_digit1,
    IResult,
};
 
fn complete_vs_streaming() {
    // Complete mode: assumes input is complete
    let mut complete_parser = tuple((digit1::<&str, nom::error::Error<&str>>, digit1));
    // digit1 in complete mode fails if there are no digits
    
    // Streaming mode: may need more input
    let mut streaming_parser = tuple((
        streaming_digit1::<&str, nom::error::Error<&str>>,
        streaming_digit1
    ));
    // digit1 in streaming mode returns Incomplete if insufficient input
    
    // tuple works with both complete and streaming parsers
    // All parsers in the tuple should use the same mode
}

tuple works with both complete and streaming input modes.

Performance Characteristics

use nom::{sequence::tuple, character::complete::digit1, IResult};
 
fn performance() {
    // tuple does NOT allocate
    // It returns a stack-allocated tuple
    
    // Each parser is called in sequence
    // No intermediate collections
    
    // The output tuple is constructed after all parsers succeed
    // If any fails, no output tuple is created
    
    let mut parser = tuple((digit1, digit1, digit1));
    
    // This is equivalent to manual sequencing:
    fn manual_parse(input: &str) -> IResult<&str, (&str, &str, &str)> {
        let (input, a) = digit1(input)?;
        let (input, b) = digit1(input)?;
        let (input, c) = digit1(input)?;
        Ok((input, (a, b, c)))
    }
    
    // tuple is zero-overhead abstraction over manual sequencing
}

tuple is a zero-overhead abstraction; it compiles to sequential parser calls.

Implementing Custom Tuple-Like Combinators

use nom::{IResult, Parser};
 
fn custom_combinator<I, O1, O2, E>(
    mut parser1: impl Parser<I, O1, E>,
    mut parser2: impl Parser<I, O2, E>,
) -> impl Fn(I) -> IResult<I, (O1, O2), E>
where
    I: Clone,
{
    move |input: I| {
        let (remaining, result1) = parser1.parse(input)?;
        let (remaining, result2) = parser2.parse(remaining)?;
        Ok((remaining, (result1, result2)))
    }
}
 
// This is essentially what tuple does internally
// The macro system handles arbitrary arities

The tuple combinator generalizes this pattern to any number of parsers.

Real-World Example: URL Parsing

use nom::{
    sequence::tuple,
    character::complete::{alpha1, digit1, char},
    bytes::complete::tag,
    combinator::opt,
    IResult,
};
 
fn url_parsing() {
    // Simplified URL: scheme://host:port/path
    
    #[derive(Debug)]
    struct Url<'a> {
        scheme: &'a str,
        host: &'a str,
        port: Option<u16>,
        path: &'a str,
    }
    
    fn parse_url(input: &str) -> IResult<&str, Url> {
        let (remaining, (scheme, _, host, port, path)) = tuple((
            alpha1,                    // scheme
            tag("://"),                // separator
            alpha1,                    // host
            opt(                       // optional port
                tuple((char(':'), digit1))
            ),
            tag("/"),                  // path start
        ))(input)?;
        
        // Remaining is the path
        Ok((remaining, Url {
            scheme,
            host,
            port: port.map(|(_, p)| p.parse().unwrap()),
            path: remaining,
        }))
    }
    
    let result = parse_url("http://example:8080/path");
    assert!(result.is_ok());
}

Complex real-world parsing combines tuple with other combinators.

Synthesis

How tuple combines parsers:

// tuple applies parsers sequentially
// (P1, P2, P3) -> applies P1, then P2, then P3
// Results are collected into tuple: (O1, O2, O3)
 
// If any parser fails:
// - The entire tuple fails
// - Input is restored to original position
// - No output tuple is created
 
// On success:
// - Returns remaining input and tuple of results
// - Each result is in the same position as its parser

Key characteristics:

// 1. Sequential: parsers run in order
// 2. Result preservation: all outputs are kept
// 3. Atomic failure: any failure fails the whole thing
// 4. Backtracking: input restored on failure
// 5. Zero overhead: compiles to sequential calls

When to use tuple:

// Use tuple when:
// - You need all parser results
// - Parsers must all succeed
// - Order matters
// - Building structured output
 
// Use alternatives when:
// - Only some values needed (preceded, delimited)
// - Pairs of values (separated_pair)
// - Optional values (opt)

Key insight: nom::sequence::tuple combines multiple parsers by applying them sequentially and collecting all results into a tuple with matching arity. It provides ordered, atomic parsing where all parsers must succeed—any failure causes the entire operation to fail with backtracking. The combinator is zero-overhead and works with any parser types that share the same input. For patterns where only some values are needed, specialized combinators like preceded, delimited, or separated_pair may be more readable, but tuple is the general-purpose building block that underlies them all.