Generic function on tuple elements

Is there a way to write a generic function that iterates over the elements of a tuple and applies the given generic function to each element individually?

The following approach does not compile:

trait TupleDo {
    fn foreach<E>(&self, handle: impl Fn(&E));
}
impl<T> TupleDo for (T,) {
    fn foreach<E>(&self, handle: impl Fn(&E)) {
        handle(&self.0);
        // calling error here
        // mismatched types
        // expected reference `&E`
        //    found reference `&T`
        // a type parameter was expected, but a different one was found; you might be missing a type parameter or trait bound
    }
}

impl<T1,T2> TupleDo for (T1,T2) {
    fn foreach<E>(&self, handle: impl Fn(&E)) {
        let e1 = &self.0;
        let e2 = &self.0;
        handle(e1); // error same as above
        handle(e2); // error same as above
    }
}

fn do_each_elment<T:Debug>(t:T) {
    println!("{t:?}");
}

fn analyze_tup<T:TupleDo>(t:T) {
    t.foreach(do_each_elment);
    // error comes here:
    // type annotations needed
    // cannot infer type of the type parameter `T` declared on the function `do_each_elment`rustcClick for full compiler diagnostic
    // lib.rs(270, 29): consider specifying the generic argument: `::<&E>`
}

fn test_tuple_do() {
    analyze_tup((3,));   
    analyze_tup((3,4));   
}

Any suggestions or insights would be greatly appreciated.

If your tuples are homogeneous, you can do something like this:

use std::fmt::Debug;

trait TupleDo<T> {
    fn foreach(&self, handle: impl Fn(&T));
}
impl<T> TupleDo<T> for (T,) {
    fn foreach(&self, handle: impl Fn(&T)) {
        handle(&self.0);
        // calling error here
        // mismatched types
        // expected reference `&E`
        //    found reference `&T`
        // a type parameter was expected, but a different one was found; you might be missing a type parameter or trait bound
    }
}

impl<T> TupleDo<T> for (T, T) {
    fn foreach(&self, handle: impl Fn(&T)) {
        let e1 = &self.0;
        let e2 = &self.0;
        handle(e1); // error same as above
        handle(e2); // error same as above
    }
}

fn do_each_elment<T: Debug>(t: &T) {
    println!("{t:?}");
}

fn analyze_tup<T: TupleDo<U>, U: Debug>(t: T) {
    t.foreach(do_each_elment);
    // error comes here:
    // type annotations needed
    // cannot infer type of the type parameter `T` declared on the function `do_each_elment`rustcClick for full compiler diagnostic
    // lib.rs(270, 29): consider specifying the generic argument: `::<&E>`
}

fn test_tuple_do() {
    analyze_tup((3,));
    analyze_tup((3, 4));
}

Playground.


If your tuples aren't homogeneous, you might be able to turn the tuple elements into trait objects, abstracting their common behaviour in a way that they can be treated homogeneously:

use std::fmt::Debug;

trait Arg: Debug {
    fn print_debug(&self) {
        println!("{self:?}");
    }
}

impl<T: Debug> Arg for T {}

trait TupleDo {
    fn foreach(&self, handle: impl Fn(&dyn Arg));
}

impl<T: Arg> TupleDo for (T,) {
    fn foreach(&self, handle: impl Fn(&dyn Arg)) {
        handle(&self.0);
        // calling error here
        // mismatched types
        // expected reference `&E`
        //    found reference `&T`
        // a type parameter was expected, but a different one was found; you might be missing a type parameter or trait bound
    }
}

impl<T1: Arg, T2: Arg> TupleDo for (T1, T2) {
    fn foreach(&self, handle: impl Fn(&dyn Arg)) {
        let e1 = &self.0;
        let e2 = &self.0;
        handle(e1); // error same as above
        handle(e2); // error same as above
    }
}

fn analyze_tup<T: TupleDo>(t: T) {
    t.foreach(|a| a.print_debug());
}

fn test_tuple_do() {
    analyze_tup((3,));
    analyze_tup((3, "hello"));
}

Playground.

6 Likes

When dealing with heterogeneous tuples, why is &dyn Any used here instead of a generic type parameter like E?
What are the limitations of using a generic E, and what makes &dyn Any necessary?

trait TupleDo {
    fn foreach(&self, handle: impl Fn(&dyn Any));
}

You can't use a generic parameter E, because E gets chosen by the caller (the place where the handle argument is defined), not the callee (TupleDo::foreach). For example, in the implementation for the generic tuple with two elements, you are passing instances of T1 and T2 to handle as arguments, not instances of E. If I—the caller—would chose E to be u8, but T1 is String, that would result in a type mismatch.

I also want to add that Any is not implemented for all types specifically types that are based on non-'static references don't implement it. This means you can't implement TupleDo for all tuples but instead only tuples for which each contained type has bound 'static. This in turn means that if I have something like a tuple that contains a non-'static str, I won't be able to call foreach on it since TupleDo isn't implemented for it.

They used &dyn Arg, not &dyn Any. I.e. a trait that encapsulates what you want to be able to do with each tuple member.

In addition to "the caller chooses E, not the callee", every generic type parameter resolves to a single type within the function body. So even if the callee could choose E, it wouldn't be possible to unify E with both T1 and T2 of a heterogeneous (T1, T2).

Your OP is a use case for "variadic generics", a feature that Rust doesn't have so far and probably won't have soon.

Again, there is no dyn Any in @jofas playground. Their implementation works for non-'static tuple members.

4 Likes

I'm well aware. I was replying to the OP's follow-up about the use of dyn Any. I assumed they meant Any and didn't misread @jofas's example of Arg—in particular they generalized the use of dyn Arg to dyn Any in an attempt for foreach to be useful for (almost) any type. I can (now) see how it was an innocent mistake though; so if they didn't mean Any, then ignore what I said. This is a case where a typo/misreading can easily be misinterpreted by someone (i.e., me) as what they actually intended.

2 Likes

Generic function cannot be parameters (nor values), you have to specify all type parameters when naming the function. But if you do not need a function but "something to handle the element" is fine, you can try the following approach:

use std::fmt::Debug;

// trait to describe "generic function", i.e. "something to handle the element"
trait GenFn {
    fn call<T: Debug>(&self, arg: &T);
}

trait TupleDo {
    fn foreach(&self, handle: impl GenFn);
}

impl<T: Debug> TupleDo for (T,) {
    fn foreach(&self, handle: impl GenFn) {
        handle.call(&self.0);
    }
}

impl<T1: Debug, T2: Debug> TupleDo for (T1, T2) {
    fn foreach(&self, handle: impl GenFn) {
        let e1 = &self.0;
        let e2 = &self.1;
        handle.call(e1);
        handle.call(e2);
    }
}

// This cannot be a function :(
struct DoEachElement;
impl GenFn for DoEachElement {
    fn call<T: Debug>(&self, t: &T) {
        println!("{t:?}");
    }
}

fn analyze_tup<T:TupleDo>(t:T) {
    t.foreach(DoEachElement);
}

fn test_tuple_do() {
    analyze_tup((3,));   
    analyze_tup((3,4));   
}

As usual, it comes with its own set of limitations.

1 Like

I think that @jofas already gave a great answer if you are ok with using dynamic dispatch.

If you want to avoid dyn and to implement a "for each" loop for a generic method over a variadic container of arbitrary size, you could use the following technique.

DISCLAIMER: It will require using a trait implementation (instead than a generic function) and a recursive container instead of a flat tuple (in the following example I am gonna use a recursive tuple).

We implement the ForEach trait over a recursive tuple of arbitrary size:

// Implement this to unlock ForEach on compatible tuples!
pub trait Handle<T> {
    fn handle(x: &T);
}

// Implementation detail...
pub trait DoForEach<Ts> {
    fn do_for_each(t: &Ts);
}

impl<F, Head, Tail> DoForEach<(Head, Tail)> for F
where
    F: Handle<Head>,
    F: DoForEach<Tail>,
{
    fn do_for_each(t: &(Head, Tail)) {
        F::handle(&t.0);
        F::do_for_each(&t.1)
    }
}

impl<F> DoForEach<()> for F {
    fn do_for_each(_t: &()) {}
}

// Use this to apply your handle over your tuple!
pub trait ForEach: Sized {
    fn for_each<F: DoForEach<Self>>(&self) {
        F::do_for_each(self);
    }
}

impl<Ts> ForEach for Ts {}

Then we can create an "Handler" by implementing the DoFn trait:

struct DebugHandler {}

impl<T: Debug> Handle<T> for DebugHandler {
    fn handle(x: &T) {
        println!("{:?}", x);
    }
}

Finally we can call for_each::<DebugHandler> over a recursive-tuple of arbitrary size.

fn analyze_tuple<T>(t: T)
where
    DebugHandler: DoForEach<T>,
{
    t.for_each::<DebugHandler>();
}

fn main() {
    // Apply DebugHandler to each element of your tuples :)
    analyze_tuple((1u8, ()));
    analyze_tuple(("hello", (1u8, ())));
    analyze_tuple(("hello", (1u8, ())));
    analyze_tuple((true, ("hello", (1u8, ()))));
}

Playground Link.

EDIT: I realized after posting that @Tom47 answer already illustrates the same idea of using a trait implementation instead of a generic function. I guess I'll leave this here since this answer shows how to implement for_each for arbitrarily sized containers and that you can avoid passing a struct as an argument (which were admittedly not requirements in the original question).

2 Likes

Thanks for the various solutions; they all work! I still have a follow-up question about @Tom47's DoEachElement approach : Why was this intermediate type (which implements Debug ) introduced rather than a generic function?