#tuple #meta-programming #variadic #generic #generics

no-std tuple_list

Crate for macro-free variadic tuple metaprogramming

4 releases

0.1.3 Jul 13, 2021
0.1.2 May 13, 2020
0.1.1 May 11, 2020
0.1.0 May 11, 2020

#1810 in Rust patterns

Download history 1315/week @ 2023-11-20 1115/week @ 2023-11-27 1370/week @ 2023-12-04 1783/week @ 2023-12-11 2141/week @ 2023-12-18 583/week @ 2023-12-25 1326/week @ 2024-01-01 1929/week @ 2024-01-08 1792/week @ 2024-01-15 1953/week @ 2024-01-22 1083/week @ 2024-01-29 1325/week @ 2024-02-05 1450/week @ 2024-02-12 2033/week @ 2024-02-19 1789/week @ 2024-02-26 2107/week @ 2024-03-04

7,443 downloads per month
Used in 12 crates (5 directly)

MIT license

52KB
651 lines

Crate tuple_list

Rust crate for macro-free variadic tuple metaprogramming.

Provides a way to recursively define traits for tuples of arbitrary size.

Please see crate documentation for details and examples.

Licensed under MIT license.


lib.rs:

Crate for macro-free variadic tuple metaprogramming.

Rationale

As of writing this crate, Rust does not support variadic generics and does not allow to reason about tuples in general.

Most importantly, Rust does not allow one to generically implement a trait for all tuples whose elements implement it.

This crate attempts to fill the gap by providing a way to recursively define traits for tuples.

Tuple lists

Tuple (A, B, C, D) can be unambiguously mapped into recursive tuple (A, (B, (C, (D, ())))).

On each level it consists of a pair (Head, Tail), where Head is tuple element and Tail is a remainder of the list. For last element Tail is an empty list.

Unlike regular flat tuples, such recursive tuples can be effectively reasoned about in Rust.

This crate calls such structures "tuple lists" and provides a set of traits and macros allowing one to conveniently work with them.

Example 1: PlusOne recursive trait

Let's create a trait which adds one to each element of a tuple list of arbitrary length, behaving differently depending on element type.

// `TupleList` is a helper trait implemented by all tuple lists.
// Its use is optional, but it allows to avoid accidentally
// implementing traits for something other than tuple lists.
use tuple_list::TupleList;

// Define trait and implement it for several primitive types.
trait PlusOne {
    fn plus_one(&mut self);
}
impl PlusOne for i32    { fn plus_one(&mut self) { *self += 1; } }
impl PlusOne for bool   { fn plus_one(&mut self) { *self = !*self; } }
impl PlusOne for String { fn plus_one(&mut self) { self.push('1'); } }

// Now we have to implement trait for an empty tuple,
// thus defining initial condition.
impl PlusOne for () {
    fn plus_one(&mut self) {}
}

// Now we can implement trait for a non-empty tuple list,
// thus defining recursion and supporting tuple lists of arbitrary length.
impl<Head, Tail> PlusOne for (Head, Tail) where
    Head: PlusOne,
    Tail: PlusOne + TupleList,
{
    fn plus_one(&mut self) {
        self.0.plus_one();
        self.1.plus_one();
    }
}

// `tuple_list!` as a helper macro used to create
// tuple lists from a list of arguments.
use tuple_list::tuple_list;

// Now we can use our trait on tuple lists.
let mut tuple_list = tuple_list!(2, false, String::from("abc"));
tuple_list.plus_one();

// `tuple_list!` macro also allows us to unpack tuple lists
let tuple_list!(a, b, c) = tuple_list;
assert_eq!(a, 3);
assert_eq!(b, true);
assert_eq!(&c, "abc1");

Example 2: CustomDisplay recursive trait

Let's create a simple Display-like trait implemented for all tuples lists whose elements implement it.

// Define the trait and implement it for several standard types.
trait CustomDisplay {
    fn fmt(&self) -> String;
}
impl CustomDisplay for i32  { fn fmt(&self) -> String { self.to_string() } }
impl CustomDisplay for bool { fn fmt(&self) -> String { self.to_string() } }
impl CustomDisplay for &str { fn fmt(&self) -> String { self.to_string() } }

// Now we have to implement trait for an empty tuple,
// thus defining initial condition.
impl CustomDisplay for () {
    fn fmt(&self) -> String { String::from("<empty>") }
}

// In order to avoid having trailing spaces, we need
// custom logic for tuple lists of exactly one element.
//
// The simplest way is to use `TupleList::TUPLE_LIST_SIZE`
// associated constant, but there is also another option.
//
// Instead of defining initial condition for empty tuple list
// and recursion for non-empty ones, we can define *two*
// initial conditions: one for an empty tuple list and
// one for tuple lists of exactly one element.
// Then we can define recursion for tuple lists of two or more elements.
//
// Here we define second initial condition for tuple list
// of exactly one element.
impl<Head> CustomDisplay for (Head, ()) where
    Head: CustomDisplay,
{
    fn fmt(&self) -> String {
        return self.0.fmt()
    }
}

// Recursion step is defined for all tuple lists
// longer than one element.
impl<Head, Next, Tail> CustomDisplay for (Head, (Next, Tail)) where
    Head: CustomDisplay,
    (Next, Tail): CustomDisplay + TupleList,
    Tail: TupleList,
{
    fn fmt(&self) -> String {
        return format!("{} {}", self.0.fmt(), self.1.fmt());
    }
}

// Ensure `fmt` is called for each element.
let tuple_list = tuple_list!(2, false, "abc");
assert_eq!(
    tuple_list.fmt(),
    "2 false abc",
);

// Since tuple lists implement `CustomDisplay` themselves, they can
// be elements in other tuple lists implementing `CustomDisplay`.
let nested_tuple_list = tuple_list!(2, false, "abc", tuple_list!(3, true, "def"));
assert_eq!(
    nested_tuple_list.fmt(),
    "2 false abc 3 true def",
);

Example 3: SwapStringAndInt recursive trait

Let's implement a trait which converts i32 to String and vice versa.

This example is way more complex that the other because it maps one tuple list into another tuple list.

// Let's define and implement a trait for `i32` and `String`
// so that it converts `String` to `i32` and vice versa.
trait SwapStringAndInt {
    type Other;
    fn swap(self) -> Self::Other;
}
impl SwapStringAndInt for i32 {
    type Other = String;
    fn swap(self) -> String { self.to_string() }
}
impl SwapStringAndInt for String {
    type Other = i32;
    fn swap(self) -> i32 { self.parse().unwrap() }
}

// Now we have to implement trait for an empty tuple,
// thus defining initial condition.
impl SwapStringAndInt for () {
    type Other = ();
    fn swap(self) -> () { () }
}

// Now we can implement trait for a non-empty tuple list,
// thus defining recursion and supporting tuple lists of arbitrary length.
impl<Head, Tail, TailOther> SwapStringAndInt for (Head, Tail) where
    Head: SwapStringAndInt,
    Tail: SwapStringAndInt<Other=TailOther> + TupleList,
    TailOther: TupleList,
{
    type Other = (Head::Other, Tail::Other);
    fn swap(self) -> Self::Other {
        (self.0.swap(), self.1.swap())
    }
}

// Tuple lists implement `SwapStringAndInt` by calling `SwapStringAndInt::swap`
// on each member and returning tuple list of resulting values.
let original = tuple_list!(4, String::from("2"), 7, String::from("13"));
let swapped  = tuple_list!(String::from("4"), 2, String::from("7"), 13);

assert_eq!(original.swap(), swapped);

Example 4: prepend and append functions

Let's implement append and prepend functions for tuple lists.

// Prepend is a trivial operation for tuple lists.
// We just create a new pair from prepended element
// and the remainder of the list.
fn prepend<T, Tail: TupleList>(value: T, tail: Tail) -> (T, Tail) {
    (value, tail)
}

// Append is a bit more comples. We'll need a trait for that.
trait Append<T>: TupleList {
    type AppendResult: TupleList;

    fn append(self, value: T) -> Self::AppendResult;
}

// Implement append for an empty tuple list.
impl<T> Append<T> for () {
    type AppendResult = (T, ());

    // Append for an empty tuple list is quite trivial.
    fn append(self, value: T) -> Self::AppendResult { (value, ()) }
}

// Implement append for non-empty tuple list.
impl<Head, Tail, T> Append<T> for (Head, Tail) where
    Self: TupleList,
    Tail: Append<T>,
    (Head, Tail::AppendResult): TupleList,
{
    type AppendResult = (Head, Tail::AppendResult);

    // Here we deconstruct tuple list,
    // recursively call append on the
    // tail of it, and then reconstruct
    // the list using the new tail.
    fn append(self, value: T) -> Self::AppendResult {
        let (head, tail) = self;
        return (head, tail.append(value));
    }
}

// Now we can use our append and prepend functions
// on tuple lists.
let original  = tuple_list!(   1, "foo", false);
let appended  = tuple_list!(   1, "foo", false, 5);
let prepended = tuple_list!(5, 1, "foo", false);

assert_eq!(original.append(5), appended);
assert_eq!(prepend(5, original), prepended);

Example 5: reverse function

We can also implement a function which reverses elements of a tuple list.

// Rewind is a helper trait which maintains two tuple lists:
// `Todo` (which is `Self` for the trait) is the remainder of a tuple list to be reversed.
// `Done` is already reversed part of it.
trait Rewind<Done: TupleList> {
    // RewindResult is the type of fully reversed tuple.
    type RewindResult: TupleList;

    fn rewind(self, done: Done) -> Self::RewindResult;
}

// Initial condition.
impl<Done: TupleList> Rewind<Done> for () {
    type RewindResult = Done;

    // When nothing is left to do, just return reversed tuple list.
    fn rewind(self, done: Done) -> Done { done }
}

// Recursion step.
impl<Done, Next, Tail> Rewind<Done> for (Next, Tail) where
    Done: TupleList,
    (Next, Done): TupleList,
    Tail: Rewind<(Next, Done)> + TupleList,
{
    type RewindResult = Tail::RewindResult;

    // Strip head element from `Todo` and prepend it to `Done` list,
    // then recurse on remaining tail of `Todo`.
    fn rewind(self, done: Done) -> Self::RewindResult {
        let (next, tail) = self;
        return tail.rewind((next, done));
    }
}

// Helper function which uses `Rewind` trait to reverse a tuple list.
fn reverse<T>(tuple: T) -> T::RewindResult where
    T: Rewind<()>
{
    // Initial condition, whole tuple list is `Todo`,
    // empty tuple is `Done`.
    tuple.rewind(())
}

// Now `reverse` is usable on tuple lists.
let original = tuple_list!(1, "foo", false);
let reversed = tuple_list!(false, "foo", 1);

assert_eq!(reverse(original), reversed);

Tuple lists and tuples interoperability

This crate defines Tuple and TupleList traits, which are automatically implemented and allow you to convert tuples into tuple lists and vice versa.

Best way to handle interoperability is to store your data as tuple lists and convert them to tuples if necessary.

Alternatively it's possible to create a helper function which accepts a tuple, converts it to a tuple list, calls trait method and then returns the result.

Here's an example of such function for Append trait from previous example:

// `Tuple` trait is needed to access conversion function.
use tuple_list::Tuple;

fn append<T, AppendedTupleList, Elem>(tuple: T, elem: Elem) -> AppendedTupleList::Tuple where
    T: Tuple,                                                   // input argument tuple
    T::TupleList: Append<Elem, AppendResult=AppendedTupleList>, // input argument tuple list
    AppendedTupleList: TupleList,                               // resulting tuple list
{
    // Convert tuple into tuple list, append the element
    // and convert the result back into tuple.
    tuple.into_tuple_list().append(elem).into_tuple()
}

// Unlike `Append` trait which is defined for tuple lists,
// `append` function works on regular tuples.
let original = (1, "foo", false);
let appended = (1, "foo", false, 5);

assert_eq!(append(original, 5), appended);

Please note that tuple/tuple list conversions are destructive and consume the original, which seemingly prevents you from, for example, modifying content of the original tuple.

In order to alleviate this problem, tuple_list crate introduces AsTupleOfRefs trait, which allows one to convert reference to tuple into tuple of references.

The idea is that if you you can convert reference to tuple into tuple of references, then convert tuple of references into tuple list and then use recursive trait as usual.

Let's modify PlusOne trait example so it can be used to modify regular tuples:

// Define trait and implement it for several primitive types.
trait PlusOne {
    fn plus_one(&mut self);
}
impl PlusOne for i32    { fn plus_one(&mut self) { *self += 1; } }
impl PlusOne for bool   { fn plus_one(&mut self) { *self = !*self; } }
impl PlusOne for String { fn plus_one(&mut self) { self.push('1'); } }

// Now we have to define a new trait
// specifically for tuple lists of references.
//
// Unlike the original, it accepts `self` by value.
trait PlusOneTupleList: TupleList {
    fn plus_one(self);
}

// Now we have to implement trait for an empty tuple,
// thus defining initial condition.
impl PlusOneTupleList for () {
    fn plus_one(self) {}
}

// Now we can implement trait for a non-empty tuple list,
// thus defining recursion and supporting tuple lists of arbitrary length.
//
// Note that we're implementing `PlusOneTupleList` for
// *tuple list of mutable references*, and as a result
// head of the list is a mutable reference, not a value.
impl<Head, Tail> PlusOneTupleList for (&mut Head, Tail) where
    Self: TupleList,
    Head: PlusOne,
    Tail: PlusOneTupleList,
{
    fn plus_one(self) {
        self.0.plus_one();
        self.1.plus_one();
    }
}

// Now let's define a helper function operating on regular tuples.
fn plus_one<'a, T, RT>(tuple: &'a mut T) where
    T: AsTupleOfRefs<'a, TupleOfMutRefs=RT>,
    RT: Tuple + 'a,
    RT::TupleList: PlusOneTupleList,

{
    tuple.as_tuple_of_mut_refs().into_tuple_list().plus_one()
}

// Now we can use this helper function on regular tuples.
let mut tuple = (2, false, String::from("abc"));
plus_one(&mut tuple);

assert_eq!(tuple.0, 3);
assert_eq!(tuple.1, true);
assert_eq!(&tuple.2, "abc1");

As you can see, working with tuples requires a lot of bolierplate code. Unless you have preexisting code you need to support, it's generally better to use tuple lists directly, since they are much easier to work with.

Implementing recursive traits for regular tuples

Implementing recursive traits for regular tuples poses certain problems. As of now it is possible within tuple_list crate, but quickly leads to orphan rules violations when used outside of it.

You can see a working example of a trait implemented for regular tuples in tuple_list::test::all_features, but it's overly complex and pretty much experimental.

It should be possible to define recursive traits on regular tuples once trait specialization feature is implemented in Rust.

No runtime deps

Features