How to mutate values in enums

I have some objects and they share some methods but all also have unique methods. As I need to store the objects in a list I have to use an enum for this but I can't find a way to implement methods on a variant so I have to use structs and then an enum to contain the structs. I'd like to be able to edit the contents of the enum generically but I'm getting a generic/trait issue that I don't understand

struct Foo {}
struct Bar {}

trait Something {}
impl Something for Foo {}
impl Something for Bar {}

struct Container<T: Something> {
    contents: T
}


enum Wrapper {
    Foo(Container<Foo>),
    Bar(Container<Bar>)
}

impl Wrapper {
    pub fn mutate<T: Something>(&mut self, method: fn(&Container<T>) -> Container<T>) {
        match self {
            Wrapper::Foo(c) => *c = method(c),
            Wrapper::Bar(c) => *c = method(c),
        }   
    }
}

(Playground)

The issue is the combination of:

  1. The arg in Wrapper::Foo(arg) has type Container<Foo>, and similar for Wrapper::Bar(arg)
  2. Wrapper::mutate() takes an arbitrary type T: Something

Armed with these, consider the following scenario:
We have a value let w = Wrapper::Foo(Container { contents: Foo });.
We then call w.mutate::<Bar>(m) where m is a fn that returns Container { contents: Bar }.

On the one hand this would seem like it should type check: Bar: Something, so what's the problem?

The problem is that c is still of type Container<Foo>, and since you're trying to write a Container<Bar> in there you get those type errors.

A key point here is that all uses of a piece of generic code should be sound and valid, so if there's a counterexample like the one I constructed above, your code won't type check.

2 Likes

You might be confused as to what generics and type parameters are.

A generic type variable can only ever stand in for a single type in a specific instantiation of a generic type or function. In your usage, you request that it stand in for two different types. This is simply not possible.

One would need higher-rank trait bounds (HRTB) over types for this to work. Unfortunately, in today's Rust, HRTBs only exist over lifetimes. You can't have a single function-typed argument be instantiated with two different type parameters.

You are also confusing generics (universal quantification) with existential types. When you have a type parameter on your function, then the caller of the function can choose it. This means that it can be whatever the caller wants, as long as it satisfies the trait bounds. But this is not what you want. You don't want to let the caller choose the types – instead, you want the function implementation itself to choose the types.

Accordingly, it makes no sense to allow any caller-chosen type. Of course, a type variable that can stand in for an arbitrary type can't be assigned to places of type Foo or Bar – what if the caller supplies a type that is not Foo or Bar?

What you are trying to do here is not directly possible, overall.

3 Likes

Ideally, there was a way to write something like

impl Wrapper {
    pub fn mutate(
        &mut self,
        method: impl for<T: Something> FnOnce(&Container<T>) -> Container<T>,
    ) {
        match self {
            Wrapper::Foo(c) => *c = method(c),
            Wrapper::Bar(c) => *c = method(c),
        }
    }
}

in Rust. Unfortunately, the language does not have such features… yet…

The most flexible stable workaround would be to use a custom trait instead of the closures, but you’d lose the convenience of closures, with automatic variable capturing and such.

trait MutateCallback {
    fn call<T: Something>(self, _: &Container<T>) -> Container<T>;
}

impl Wrapper {
    pub fn mutate(&mut self, method: impl MutateCallback) {
        match self {
            Wrapper::Foo(c) => *c = method.call(c),
            Wrapper::Bar(c) => *c = method.call(c),
        }   
    }
}

To make calling this API easier for an existing generic function item, you could offer a convenience macro

macro_rules! mutate_callback {
    ($function:expr) => {{
        struct __S;
        impl $crate::MutateCallback for __S {
            fn call<T: $crate::Something>(self, x: &$crate::Container<T>) -> $crate::Container<T> {
                $function(x)
            }
        }
        __S
    }};
}

fn some_callback_function<T: Something>(x: &Container<T>) -> Container<T> {
    Container {
        contents: x.contents.clone()
    }
}

fn demo(mut x: Wrapper) {
    x.mutate(mutate_callback!(some_callback_function))
}

Rust Playground

But anything involving capturing variables will probably need a hand-implemented closure-equivalent. (And/or maybe more advanced macros?)

4 Likes

Guess I was a bit tired or something because, yes, obviously you're right and it would never have worked.

Thanks, I'll see what I can do to use this