When you don't know where to put the method: implement it on a tuple?

Sometimes it's not clear where to put a method. For example:

Of course, you could create a free standing function in those cases. But then you don't use the advantages of method style syntax. Or you could implement the method on each and provide a wrapper for the opposite choice (as done here by the rand crate).

I wondered, is it also possible (and useful / idiomatic / reasonable) to implement methods on tuples, like this:

impl<V, K> Startable for (&mut V, &K)
where
    V: Vehicle,
    K: VehicleKey<V>,
{
    fn start(self) {
        let (vehicle, _key) = self;
        // we don't see immediately that `vehicle` is in fact a mutable reference
        vehicle.set_started(true);
        println!("{}💨", vehicle.repr());
    }
}

fn main() {
    let mut car = Car { started: false };
    let key = CarKey;
    (&mut car, &key).start();
    assert_eq!(car.started, true);
}

(Playground)

Output:

🚗💨

I feel like it will rather get me into problems than making things easier, but what is your opinion on that?

2 Likes

A free-standing function would work better and be simpler. I don't see any benefit here from using a method.

2 Likes

A concrete use case is, for example, the sample methods of the rand crate (as linked in my OP), or methods that are currently part of a transaction in mmtkvdb. Moving them (either the sample method of the rand crate or the methods being part of the transactions in mmtkvdb) to be free-standing functions would reduce ergonomics for the caller I guess or require more names in the top-level scope.

Compare:

distribution.sample(&mut seed)

vs

seed.sample(&distriution)

vs

rand::sample(&mut seed, &distribution)

vs

sample(&mut seed, &distribution)

vs

(&mut seed, &distribution).sample()

Why not

(&distribution, &mut seed).sample()

? I feel like the order selected is as arbitrary as which type the method belongs to. You could do the same thing as rand of course and do both, but I don't see the advantage over implementing it as a method for both types.

1 Like

Yeah, I guess that diminishes any advantage.

However, it does keep namespace a bit cleaner. Consider:

struct A;
struct B;
struct C;

trait Foo {
    fn foo(self);
}

impl Foo for (&A, &B) {
    fn foo(self) {
        println!("One");
    }
}

impl Foo for (&A, &C) {
    fn foo(self) {
        println!("Two");
    }
}

impl Foo for (&B, &C) {
    fn foo(self) {
        println!("Three");
    }
}

fn main() {
    let (a, b, c) = (A, B, C);
    (&a, &b).foo();
    (&a, &c).foo();
    (&b, &c).foo();
}

(Playground)

Implementing Foo for (&A, &B) will not collide with the implementation for (&A, &C) or (&B, &C), which may provide entirely different functionality under the same method name.

2 Likes

which may provide entirely different functionality under the same method name.

Different functionality implemented using the same trait? That sounds like it will add confusion, unless the trait is explicitly as nonspecific as, say, Fn is.

I think that when it is unclear, in the sense of “not yet clarified”, where a method should go, the correct answer is to use a free-standing function until you know more, and the right final answer may be to just keep it that way.

1 Like

Depends on how different it is. You could also do:

struct A;
struct B;
struct C;

trait One {
    fn foo(self);
}

impl One for (&A, &B) {
    fn foo(self) {
        println!("One");
    }
}

trait Two {
    fn foo(self);
}

impl Two for (&A, &C) {
    fn foo(self) {
        println!("Two");
    }
}

trait Three {
    fn foo(self);
}

impl Three for (&B, &C) {
    fn foo(self) {
        println!("Three");
    }
}

fn main() {
    let (a, b, c) = (A, B, C);
    (&a, &b).foo();
    (&a, &c).foo();
    (&b, &c).foo();
}

(Playground)

Let's try to look at two concrete examples:

Questions:

Should these really be free standing functions, or is it clear where to put the method in each case? How idiomatic is what the rand crate does?

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.