Skip to content

Instantly share code, notes, and snippets.

@Jules-Bertholet
Created June 29, 2023 16:00
Show Gist options
  • Save Jules-Bertholet/67690e82a3041d7845b2bb816158576f to your computer and use it in GitHub Desktop.
Save Jules-Bertholet/67690e82a3041d7845b2bb816158576f to your computer and use it in GitHub Desktop.
Variadics old.md

Variadics design sketch

Use-cases we want to support

Design principles

  • Re-use existing language infrastructure and concepts when possible (tuples, pattern matching).
    • But also, support library types!
  • Don't support only pre-determined special cases, support all the cases.
    • Provide composable building blocks.
    • Try to be flat when possible, but allow nesting when needed.
    • Lifetime and const generics should be first-class citizens.
    • "Complex is better than complicated."
  • Modular - try to have MVP-able subset, with confidence that it won't conflict with a more complete feature.
  • Try to keep simple cases simple.
  • "Explicit is better than implicit."
    • No implicit iteration! Ugly loops are preferable to beautiful bugs.
    • It should be hopefully be clear whether an identifier refers to one thing or N things, wherever it is used.

Prior art

Other links

Variadics and values

... operator

... ("unpack") is a prefix operator that unpacks a parenthesized list of values. It operates on tuples, tuple structs, arrays, and references to them. (For tuple structs, all the fields must be visible at the location where ... is invoked. Also, if the struct is marked non_exhaustive, then unpacking only works within the defining crate.)

TODO: should it be called "unpack", "splat", "spread", or something else? TODO: should it be postfix instead?

let a: (i32, u32) = (3, 4);
let b: (i32, u32, usize) = (...a, 1);
let c: (usize, i32, u32) = (1, ...a);
struct Foo(bool, bool);
let d: (bool, bool) = (...Foo(true, false));
let e: Foo = Foo(...d);
let f: [i32, 3] = [1, ...[2, 3]];
let g: (i32, i32, i32) = (1, ...(2, 3));
let h: Foo: = Foo(...[true, false]);
let i: [i32, 3] = [1, ...(2, 3)];

Unpacking a reference results in references.

let j: (i32, &usize, &bool) = (3, ...&(4, false));
let k: (i32, &mut usize, &mut bool) = (3, ...&mut (4, false));

... pattern

... can also be used in patterns.

Arrays

For arrays, ... patterns serve as an alternative to ident @ ...

let a: [i32, 3] = [1, 2, 3];
let [...head, tail] = a;
assert_eq!(head, [1, 2]);
let [ref ...head, tail] = a;
let _: &[i32; 2] = head;
let [ref mut ...head, tail] = a;
let _: &mut [i32; 2] = head;

Tuples

... patterns also work with tuples.

let t: (i32, f64, &str) = (1, 2.0, "hi");
let (...head, tail) = t;
assert_eq!(head, (1, 2.0));

By-reference binding modes are a little special, owing to how tuples are stored in memory.

let t: (i32, f64, &str) = (1, 2.0, "hi");

let (ref ...head, tail) = t;
// Tuple of references, not reference to tuple
let _: (&_, &_) = head;

// Match ergonomics
let &(...head, tail) = t;
// Tuple of references, not reference to tuple
let _: (&_, &_) = head;

TODO: extend ident @ .. patterns to support this? TODO: allow using ... patterns with slices as well?

Tuple structs

Same as tuples.

struct Foo(i32, f64, &str);

let t: Foo = Foo(1, 2.0, "hi");
let Foo(...head, tail) = t;
assert_eq!(head, (1, 2.0));

let Foo(ref ...head, tail) = t;
let _: (&_, &_) = head;

let &Foo(...head, tail) = t;
let _: (&_, &_) = head;

static for loop

static for loops allow iterating over the elements of am unpackable value (tuple, tuple struct, array, references to these). The loop is unrolled at compile-time.

TODO: bikeshed syntax.

let mut v: Vec<Box<dyn Debug>> = Vec::new();

static for i in (2, "hi", 5.0) {
    v.push(Box::new(i));
}

dbg!(&v); // [2, "hi", 5.0]
let mut v: Vec<i32> = Vec::new();
let mut a = [2, 3, 5];

static for i in a {
    v.push(i);
}

dbg!(&v); // [2, 3, 5]
let mut v: Vec<i32> = Vec::new();
let mut a = [2, 3, 5];

static for i in (...a, 4) {
    v.push(i);
}

assert_eq!(v, [2, 3, 5, 4])

static for loops evaluate to a tuple.

let _: (Option<u32>, Option<bool>) = static for i in (42, false) {
    Some(i)
};

You can continue with a value.

let _: (Option<u32>, Option<bool>) = static for i in (42, false) {
    if !pre_check() {
        continue None;
    }

    Some(expensive_computation(i))
};

continue; with no value specified is equivalent to continue ();.

static for i in (42, false) {
    if !pre_check() {
        continue;
    }

    expensive_operation(i);
}

static for can also contain a break statement; this changes the return type of the loop to (). Such a loop only supports bare continue;, with no value provided.

static for i in (42, false) {
    if we_are_done() {
        break;
    }

    some_operation(i);
}

You can static for over multiple unpackable values at once, as long as they are the same length.

let t: ((i8, u8), (i16, u16), (i32, u32)) = static for i, u in (-1, -2, -3), (1, 2, 3) {
    (i, u)
};

assert_eq!(t, ((-1, 1), (-2, 2), (-3, 3)));
// ERROR incompatible lengths
// static for i, u in (-1, -2, -3), (1, 3) {};

... patterns can also be used for working with multiple values at once. When used after for, the resulting binding has tuple type.

let t: ((i8, u8), (i16, u16), (i32, u32)) = static for ...tup in (-1, -2, -3), (1, 2, 3) {
    tup
};

assert_eq!(t, ((-1, 1), (-2, 2), (-3, 3)));
let t: ((i8, u8), (i16, u16), (i32, u32)) = static for ...tup in ...((-1, -2, -3), (1, 2, 3)) {
    tup
};

assert_eq!(t, ((-1, 1), (-2, 2), (-3, 3)));

Variadics and types

Generic argument packs

A generic argument is a type, lifetime, or constant. A generic argument pack is a comma-separated list of zero or more generic arguments, including nested packs. Lifetimes in a pack must be listed first.

Examples of generic argument packs:

  • <usize, bool>
  • <'a, &'a u32, 23.7_f64>
  • <usize,> (trailing comma required)
  • <>
  • <'a, '_, u32, <'b, 3_u32, &'b T>, ()>

Generic pack parameter

In today's Rust, generic parameters come in several forms.

Declare Fill Result
<T> <i32> T = i32
<T, U> <i32, u32> T = i32, U = u32
<'a, 'b> <'_, 'static> 'a = '_, 'b = 'static
<const A: u32, const B: isize> <3, 5> A = 3_u32, B = 5_isize

A generic pack parameter is a generic parameter that is bound to a generic pack of a particular form. ... is used to declare such parameters.

Declare Fill Result
<...Ts> <usize, u32, bool> Ts = <usize, u32, bool>
<<...Ts>, <...Us>> <<i32, isize>, <u32, usize, bool>> Ts = <i32, isize>, Us = <u32, usize, bool>
<<...'as>, <...'bs>> <<'static, '_>, <'a>> 'as = <'static, '_>, 'bs = <'a>
<T, <...const As: T>, <const Bs: T>> <i32, <-1, 0, 1>, <42>> T = i32, As = <-1_i32, 0_i32, 1_i32>, Bs = <42_i32>
<...<'as, Ts: 'as, const Ns: Ts>> <<'static, i32, -4>, <'_, bool, false>> 'as = <'static, '_>, Ts = <i32, bool>, Ns = <-4_i32, false>
<'a, ...'bs, ...Ts, ...<'cs, Us>> <'static, '_, 'b, u32, i64, <'static, u32>, <'static, i32>> 'a = 'static, 'bs = <'_, 'b>, Ts = <u32, i64>, 'cs = <'static, 'static>, Us = <u32, i32>
<...<...<'as, Ts: 'as>>> <<<'static, isize>, <'a, & 'a u32>>, <<'b, &'b mut usize>>> 'as = <<'static, 'a>, <'b>>, Ts = <<isize, &'a u32>, <&'b mut usize>>
<F, ...Ts, U> <isize, u32, f64, usize> F = isize, Ts = <u32, f64>, U = usize
<F, ...Ts> <u32> F = u32, Ts = <>

Sequences of generic pack parameters that are structurally ambiguous are forbidden. For example, <...Ts, ...Us> is not allowed, because it would then be unclear at the fill site what belongs in Ts vs Us. Use nesting instead: <<...Ts>, <...Us>>.

Type-level ...

Type-level ... unpacks a generic argument pack.

This feature is meant for use with variadic generics, so the concrete examples below will be underwhelming.

//     ╭─ `(i32, u32)`, but written in an overly verbose fashion.
//    ╭┴──────────────╮
let t: (...<i32, u32>) = (2, 3);
//     ╭─ `(i32, u32, usize)`, but written in an overly verbose fashion.
//    ╭┴─────────────────────╮
let t: (...<i32, u32>, isize) = (2, 3, -4);

static for over types, lifetimes, and consts

static for can also be used to iterate over the members of a generic argument pack.

TODO: bikeshed.

// `type` keyord is required for disambiguation
let t: (usize, i32) = static for type T in <usize, i32> {
    T::default();
};

assert_eq!(t, (0, 0))
static for 'x in <'static, 'a> {
    // Not much interesting you can do with only a lifetime...
    // But these will get more useful in later examples.
    let _: &'x i32 = &42;
};
// TODO: should type annotation on `const N` be required?
let t: (usize, usize, usize) = static for const N: usize in <3, 17, 21> {
    N + 1
};

assert_eq!(t, (4, 18, 22));
let t: (usize, i32, bool) = static for type T, const N: T in <usize, i32, bool>, <3, -10, true> {
    N + T::default()
};

assert_eq!(t, (3, -10, true));
// TODO: "There should be one-- and preferably only one --obvious way to do it."
let t: (usize, i32, bool) = static for <type T, const N: T> in <<usize, 3>, <i32, -10>, <bool, true>> {
    N + T::default()
};

assert_eq!(t, (3, -10, true));
// TODO: should `type` keyword be required?
let t: ((usize, i32), (bool,)) = static for <...type Ts>, <...const Ns: Ts> in <<usize, i32>, <bool,>>, <<3, -10,>, <true,>> {
    static for type T, const N: T in Ts, Ns {
        N + T::default()
    }
};

assert_eq!(t, ((3, -10), (true,)));

static for can also work with both unpackable values and generic argument packs at the same time:

let t: ((i32, bool), (u32, usize), (i32, u8)) = static for val, type T in (1, 2, 1), <bool, usize, u8> {
    (val, T::default())
};

assert_eq!(t, ((1, false), (2, 0), (1, 0)));

Type-level for-in

Type-level for-in maps a generic argument pack to a new pack of the same length.

Once again, for use with variadic generics, so the concrete examples below will be underwhelming.

//     ╭─ `(Option<i32>, Option<u32>)`, but written in an overly verbose fashion.
//    ╭┴──────────────────────────────────╮
let t: (...for<T in <i32, u32>> Option<T>) == (Some(2), Some(3));
//     ╭─ `([bool; 3], [bool; 12])`, but written in an overly verbose fashion.
//    ╭┴────────────────────────────────────────────╮
let t: (...for<const N: usize in <8, 12>> [bool; N]) = ([false; 3], [false; 0]);
//     ╭─ `([bool; 3], [char; 1])`, but written in an overly verbose fashion.
//    ╭┴─────────────────────────────────────────────────────────────╮
let t: (...for<<T, const N: usize> in <<bool, 3>, <char, 1>>> [T; N]) = ([false; 3], ['c']);
//     ╭─ `(usize, (u32, i32, usize), (bool, char))`, but written in an overly verbose fashion.
//    ╭┴────────────────────────────────────────────────────────────────────╮
let t: (usize, ...for<<...Ts> in <<u32, i32, usize>, <bool, char>>> (...Ts)) = (4, (1, 7, 4), (true, 'c'));

.. inferred type parameter lists

In today's Rust, you can mark type parameters as inferred with _.

let a: Option<_> = Some(3_u32);

With this proposal, you can use .. to infer a list of type parameters.

// `..` is equivalent to `_, _` below
let tup: (usize, ..) = (3, false, "hello");

Functions with variadic generic parameters

Finally, the good stuff.

This section will just be examples, showing how the concepts we've introduced so far fit together.

/// Takes a tuple and returns it unmodified.
//
//     ╭─ Declare variadic generic parameter of zero or more types.
//     │
//     │             ╭─ A tuple of all the types in `Ts`.
//     │             │
//     │             ├────────────╮
//    ╭┴────╮       ╭┴──────╮    ╭┴──────╮  
fn id< ...Ts >( vs : (...Ts) ) -> (...Ts) {
    vs
}

#[test]
fn test_id() {
    assert_eq!(id(()), ());
    assert_eq!(id((3,)), (3,));
    assert_eq!(id((42, "hello world")), (42, "hello world"),);
    assert_eq!(id((false, false, 0)), (false, false, 0));
    assert_eq!(id::<[u8; 9], &str>((b"t u r b o", "f i s h")), (b"t u r b o", "f i s h"));
}
/// Does the same thing as `id`, but the tuple must have arity at least 1.
fn id_at_least_one<H, ...Ts>(tuple: (H, ...Ts)) -> (H, ...Ts) {
    tuple
}

#[test]
fn test_id_at_least_one() {
    // assert_eq!(id_at_least_one(()), ()); ERROR expected at tuple of length at least one
    assert_eq!(id_at_least_one(3), (3,));
    assert_eq!(id_at_least_one((42, "hello world")), (42, "hello world"));
    assert_eq!(id_at_least_one((false, false, 0)), (false, false, 0));
    assert_eq!(id_at_least_one::<[u8; 9], &str>((b"t u r b o", "f i s h")), (b"t u r b o", "f i s h"));
}
/// Wraps all the elements of the input tuple in `Some`.
//
//                                          ╭─ Map each type `T` in the tuple to a corresponding `Option<T>`.
//                                          │  Type-level for-in with generics.
//                                         ╭┴─────────────────────╮
fn option_wrap<...Ts>(vs : (...Ts)) -> (... for<T in Ts> Option<T> ) {
    let wrapped: (...for<T in Ts> Option<T>,) = static for v in vs {
        Some(v)
    };

    wrapped
}

#[test]
fn test_option_wrap() {
    assert_eq!(option_wrap(()), ());
    assert_eq!(option_wrap((3,)), (Some(3),));
    assert_eq!(option_wrap((42, "hello world")), (Some(42), Some("hello world")));
    assert_eq!(option_wrap((false, false, 0)), (Some(false), Some(false), Some(0)));
    assert_eq!(option_wrap::<[u8; 9], &str>((b"t u r b o", "f i s h")), (Some(b"t u r b o"), Some("f i s h")));
}
/// Move the first element of the tuple to the end.
fn swing_around<H, ...Ts>((head, ...tail): (H, ...Ts)) -> (...Ts, H) {
  //  ╭─ Use value-level unpack operator to construct the tuple.
  // ╭┴──────╮
    ( ...tail , head)
}

#[test]
fn test_swing_around() {
    //swing_around(()); ERROR expected tuple of arity >= 1, found 0
    assert_eq!(swing_around((3,)), (3,));
    assert_eq!(swing_around((42, "hello world")), ("hello world", 42));
    assert_eq!(swing_around((false, false, 0)), (0, false, false));
    assert_eq!(swing_around::<[u8; 9], &str>((b"t u r b o", "f i s h")), ("f i s h", b"t u r b o"));
}
/// Push `17` onto the end of the tuple.
fn add_one_on<...Ts>(vs : (...Ts)) -> (...Ts, u32) {
    (...vs, 17)
}

#[test]
fn test_add_one_on() {
    assert_eq!(add_one_on(()), (17,));
    assert_eq!(add_one_on((3,)), (3, 17));
    assert_eq!(add_one_on((42, "hello world")), (42, "hello world", 17));
    assert_eq!(add_one_on((false, false, 0)), (false, false, 0, 17));
    assert_eq!(add_one_on::<[u8; 9], &str>((b"t u r b o", "f i s h")), (b"t u r b o", "f i s h", 17));
}

You can put trait bounds on variadic generic parameters.

/// Return a tuple with the specified element types, generating each element value by calling `Default::default()`.
//          ╭─ Every type in `Ts` must meet the `: Default` bound.
//         ╭┴─────────────╮
fn default< ...Ts: Default >() -> (...Ts) {
    // `static for` with variadic generic type.
    static for type T in Ts {
        T::default()
    }
}

#[test]
fn test_default() {
    assert_eq!(default::<>(), ());
    assert_eq!(default::<u32>(), (0,));
    assert_eq!(default::<u32, bool>(), (0, false));
}

Variadic generic parameters can come in front of non-variadic ones.

fn is_last_3<...Ts, Last: PartialEq<i32>>((...front, last): (...Ts, Last)) -> (...Ts, bool) {
    (...front, last == 3)
}

#[test]
fn test_is_last_3() {
    assert_eq!(is_last_3(("yeah", "yeah", 3)), ("yeah", "yeah", true));
}

Where clauses are also supported, via for-in.

/// Returns `true` iff 3 is equal to every element of the given tuple.
fn all_3<...Ts>(vs : (...Ts)) -> bool
where
    /// Every type `T` in `Ts` meets the specified bound.
    for<T in Ts> i32: PartialEq<T>,
{
    static for v in vs {
        if 3_i32 != v {
            return false;
        }
    }

    true
}

#[test]
fn test_all_3() {
    assert_eq!(all_3(()), true);
    assert_eq!(all_3((3, 3, 3)), true);
    assert_eq!(all_3((3, 17, 3)), false);
}
/// Collect the provided const generic params into a `for` loop.
fn collect_consts<...const Ns: usize>() -> (...for<const N: usize in Ns> usize) {
    static for const N: usize in Ns {
        N
    }
}

#[test]
fn test_collect_consts() {
    assert_eq!(collect_consts::<>(), ());
    assert_eq!(collect_consts::<3, 8, 7>(), (3, 8, 7));
}

Variadics add no new post-monomprphization errors, all possible instantiations must be valid.

fn foo<...Ts>(tup: (...Ts)) {
    // drop(...tup); // ERROR: `drop` is a function of arity 1, but `tup` could have any arity
}

We now have enough to implement several traits for tuples of any length.

impl<...Ts: fmt::Debug> fmt::Debug for (...Ts) {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        let mut list = f.debug_list();

        static for elem in self {
            f.entry(elem);
        }
        
        list.finish()
    }
}
impl<...Ts: Default> Default for (...Ts) {
    fn default() -> Self {
        static for type T in Ts {
            T::default()
        }
    }
}

... patterns in function parameter lists

... patterns can also be used in function parameter lists.

As is our tradition, we start with a contrived concrete example before introducing generics.

//                    ╭─ This binding has tuple type.
//                    │  However, in terms of calling convention,
//                    │  elements are passed individually.
//                    │  TODO: allow `tuple @ ..` as alternative syntax?
//                    │
//                    │           ╭─ No need to unpack or parenthesize.
//                   ╭┴────────╮ ╭┴─────────╮
fn collect_contrived( ... tuple : <u32, i32> ) -> (u32, i32) {
    tuple
}

// The above and below functions are exactly equivalent, in signature, semantics, and calling convention.

fn collect_contrived(a: u32, b: i32) -> (u32, i32) {
    (a, b)
}

When using a ... binding with unsized argument types via unsized_fn_params, there are additional restrictions.

#![feature(unsized_fn_params)]

fn do_stuff_with_unsized_params(...tuple: <[u32], dyn Debug>) {
    // `tuple` is still a tuple, sort of.
    // But: you aren't allowed to take its address, or pass it to a function by value.
    // The only thing you can do is index into its fields;
    // either directly, via pattern match, or with `static for`.
    // These restrictions won't be lifted even if `([u32], dyn Debug)` gets a defined layout,
    // but they may be with `#![feature(unsized_locals)]`.

    dbg!(&tuple.0[0]);
    dbg!(&tuple.1);
}

Varargs

Combining function parameter ... and variadic generics gives us varargs.

/// Takes a variadic set of type arguments,
/// returns those arguments collected into a tuple.
fn collect<...Ts>(...vs: Ts) -> (...Ts) {
    vs
}

#[test]
fn test_collect() {
    assert_eq!(collect(), ());
    assert_eq!(collect(3), (3,));
    assert_eq!(collect(42, "hello world"), (42, "hello world"));
    assert_eq!(collect(false, false, 0), (false, false, 0));
    assert_eq!(collect::<[u8; 9], &str>(b"t u r b o", "f i s h"), (b"t u r b o", "f i s h"));
}
/// Does the same thing as `id`, but you have to pass in at least one value.
fn collect_at_least_one<H, ...Ts>(head: H, ...tail: Ts) -> (H, ...Ts) {
    (head, ...tail)
}

#[test]
fn test_collect_at_least_one() {
    // assert_eq!(collect_at_least_one(), ()); ERROR expected at least 2 parameters, found 1
    assert_eq!(collect_at_least_one(3), (3,));
    assert_eq!(collect_at_least_one(42, "hello world"), (42, "hello world"));
    assert_eq!(collect_at_least_one(false, false, 0), (false, false, 0));
    assert_eq!(collect_at_least_one::<[u8; 9], &str>(b"t u r b o", "f i s h"), (b"t u r b o", "f i s h"));
}

Multiple variadic arguments can follow one another, but turbofish is then required for disambiguation. TODO: bikeshed above statement.

fn double_vararg<<...Ts>, <...Us>>collect_twice(...fst: Ts, ...lst: Us) -> ((...Ts), (...Us)) {
    (fst, lst)
}

#[test]
fn test_double_vararg() {
    assert_eq!(double_vararg::<<i32, u32>, <usize,>>(1, 2, 3), ((1, 2), (3,)));
    assert_eq!(double_vararg::<<i32,>, <u32, usize>>(1, 2, 3), ((1,), (2, 3)));
}

Variadics with ADTs

struct Tup<...Ts>(i32, ...Ts);

impl<...Ts> Tup<...Ts> {
    fn new(i: i32, ...ts: Ts) {
        Self(i, ...ts)
    }

    fn foo(&self) {
        dbg!(self.0);
        // dbg!(self.1); ERROR: that field might not exist
    }
}
// `iter::Zip`

pub struct Zip<...Is>(...Is)

impl<...Is> Zip<...Is> {
    fn new(...iters: Is) {
        Self(...iters)
    }
}

impl<...Is: Iterator> Iterator for Zip<...Is> {
    type Item = (...for<I in Is> I::Item);

    fn next(&mut self) -> Option<Self::Item> {
        let next: Self::Item = static for i in self {
            i.next()?
        };

        Some(next)
    }
}
// `futures::join`

use  futures_util::future::{MaybeDone, maybe_done};

#[pin_project::project]
pub struct Join<...Futs: Future>(#[pin] ...for<F in Futs> MaybeDone<F>);

impl<...Futs> Join<...Futs> {
    fn new(...futures: Futs) {
        let wrapped_futs = static for future in futures {
            maybe_done(future)
        };

        Self(...wrapped_futs)
    }
}

impl<...Futs: Future> Future for Join<...Futs> {
    type Output = (...for<F in Futs> F::Output);

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let mut all_done = true;

        // TODO: what is the best API for pin projection?

        // Long, annoying type specified for example purposes.
        // In reality, you would infer it with `(..)`.
        let futs: (...for<F in Futs> Pin<&mut F>) = self.project();

        static for fut in futs {
            all_done &= fut.poll(cx).is_ready();
        }

        if all_done {
            let ready = static for fut in futs {
                fut.take_output().unwrap()
            };

            Poll::Ready(ready)
        } else {
            Poll::Pending
        }
    }
}
// `FnOnce`

trait FnOnce<...Args> {
    type Output;

    fn call_once(self, ...args: Args) -> Self::Output;
}

struct Foo;

// No variadic syntax here!
impl FnOnce<u32, i32, i32> for Foo {
    type Output = u32;

    fn call_once(self, a: u32, b: i32, c: i32) -> u32 {
        a + ((b + c) as u32)
    }
}

Advanced examples

// Implement non-symmetric `PartialEq` for tuples

impl<...<Ts: PartialEq<Us>, Us>> PartialEq<(...for<U in Us> U)> for (...for<T in Ts> T) {
    fn eq(&self, other: &(...for<U in Us> U)) {
        static for l, r in self, other {
            if l != r {
                return false;
            }
        }

        true
    }
}
/// Pair of tuples → tuple of pairs
pub fn zip<...<Ts, Us>>(left: (...for<T in Ts> T), right: (...for<U in Us> U)) -> (...for<T, U in Ts, Us> (T, U)) {
    static for l, r in left, right {
        (l, r)
    }
}
/// Tuple of pairs → pair of tuples
pub fn unzip<...<Ts, Us>>(zipped: (...for<T, U in Ts, Us> (T, U))) -> ((...for<T in Ts> T), (...for<U in Us> U)) {
    // This one is a bit mind-bending.
    // TODO: could it be made more intuitive?
    static for ...elems in ...zipped {
        elems
    }
}

TODO: match and recursion

static for is not the only way to loop over a tuple. You can also use recursion, with match.

/// Debug-print every element of the tuple.
fn recursive_dbg<...Ts: Debug>(ts: (...Ts)) {
    match ts {
        () => (),
        (head, ...tail) => {
            print!("{head:?}, ");
            recursive_dbg(tail);
        }
    }
}

Recursion also allows iterating in a different order.

/// Debug-print every element of the tuple, in reverse order.
fn recursive_dbg<...Ts: Debug>(ts: (...Ts)) {
    match ts {
        () => (),
        (...head, tail) => {
            print!("{tail:?}, ");
            recursive_dbg(head);
        }
    }
}

However, how do we express this recursion at the type level?

/// Reverse the order of the elements in the tuple.
fn reverse_tuple<...Ts: Debug>(ts: (...Ts)) -> _ /* what do we put here? */ {
    match ts {
        () => (),
        (head, ...tail) => {
           (...reverse_tuple(tail), head)
        }
    }
}

A more generalized type-level recursion mechanism/match would be the most flexible option, but also complex. Alternatively, we could have a set of primitives, like Rotate<...T> or Reverse<...T>, for type-level operations. Also, what restrictions would need to be imposed to make type inference tractable, and avoid post-monomorphization errors? How would unification work?

It's also not clear how the match + recursion approach could be applied to iterating over types/lifetimes/consts at the value level.

Type-level match

What might type-level match look like?

...type Reverse<...Ts> = match Ts <
    // Restriction: lifetimes can't affect which branch is taken, otherwise you would have unsound specialization
    <> => <>,
    <Head, ...Tail> => <...Reverse<Tail>, Head>,
>;

/// Reverse the order of the elements in the tuple.
fn reverse_tuple<...Ts: Debug>(ts: (...Ts)) -> (...Reverse<...Ts>) {
    match ts {
        () => (),
        (head, ...tail) => {
           (...reverse_tuple(tail), head)
        }
    }
}

TODO: Pack length constraints

It would be desirable to allow constraining the length of a generic parameter pack. For example, to write a generalized MxN → NxM nested tuple transformation, you need to assert that all the inner tuples are of the same length. Speculative syntax:

/// Tranform MxN tuple into NxM
//                                   ╭─ Each `Ts` in `Tss` has `N` elements
//                                  ╭┴╮
fn pivot<const N: usize, ...<...Tss; N >>(tuples: (...for<<...Ts> in Tss> (...Ts))) {
    static for ...t in ...tuples {
        t
    }
}

TODO: Random access

It would be nice to have a way to express "get the Nth value of this tuple, if it exists", where N is a const generic parameter. One would also need a type-level equivalent for generic parameter packs.

(People have expressed this desire in feedback to the author, but I would like to see concrete use-cases.)

TODO: Variadic variant lists with enums

With APIs like futures::select, you want to pass in N values, and get back a value corresponding to one of the N arguments. Yoshua Wuyts's blog posts explore this in detail. To support this case, one might want enum types whose variats correspnd to variadic generic parameters. But perhaps the additional complexity is not necessary; for example, in futures::select we could instead ask the caller to wrap the result type of their futures in an enum they define.

TODO

  • Add additional advanced examples
  • Resolve inline bikeshed TODOs
  • Add desgin for:
    • Higher-ranked and elided lifetimes
    • impl Trait
    • GATs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment