Fun with traits, generics and complex types in Rust: Pass either a new value or compute the new value from a closure to a method
7 May, 2025
I have a type Person
as follows:
I would like to have an API that allows me to update its age
field either by specifying a concrete value or updating it with a closure:
let person = Person ;
let years = 11;
// set its age to a value
assert_eq!;
// update its age by passing a closure
assert_eq!;
I know that you can do this sort of crazy, beautiful type-level dark magic using traits. I'm pretty new to Rust type-fu, but I had a go at creating an Updater
trait that can do this:
This is a generic trait with a blanket impl for all FnOnce
closures that take 1 parameter u8
and return u8
.
I can then create my method like so:
And, it works! But, what if instead my Person
is a more complex type:
I want to create a similar updater method for each field, I don't want to create a new trait for each field, that would totally not be worth it.
I'd just like to have 1 trait which takes a generic type parameter, which would allow me to create those methods like so:
To achieve the above, I tried making my trait implementation generic.
Either of them work, but not both at the same time. Rust says that the trait implementations are conflicting. I learned that this specific problem would be solved by something called specialization.
Essentially, my generic trait blanket implementations don't work because the set of types that are FnOnce(T) -> T
are a subset of the set of types that are T
.
Essentially, a type like |x| x + 4
which is a closure FnOnce(u8) -> u8
, gets both implementations:
impl<T> Updater<T> for T
impl<T, F: FnOnce(T) -> T> Updater<T> for F
And this is a problem in Rust because it can't choose a "more specific" implementation and instead the compiler thinks |x| x + 4
has 2 implementations of the same trait which should not be possible.
This is where specialization would come in to help. At the moment there does not seem to be a sound implementation of specialization and it may take a lot of work to get there.
Thankfully, we don't need specialization here! What we can instead do, is guarantee to the type checker that the two blanket implementations never overlap by introducing generic parameters that are disjoint from one another.
We can do that like this:
// This type can never be constructed. It is only a type-level marker
// This type can also never be constructed. It is only a type-level marker
Because AbsoluteMarker
and RelativeMarker
never overlap, the Rust compiler is able to reason that the two trait implementations also do not overlap.
We would expect to have to call the age
method like this:
person.;
person.;
But we don't need to do that! Rust's type inference system is smart enough to infer these generics for us.
// This works!
person.age;
person.age;
Thanks to u/xr2279 for showing me how to do that.
Should you do this?
Probably not. It is fairly complex and it may just be better to write the 2 methods. It depends on your need, of course. But you can accomplish the above with a public field:
person.age = 4;
person.age += 4;
The way that we did it allows us to write the updates in a functional style, so maybe it's something you might prefer when chaining many update methods together.
rectangle.x.y.width.height
It's very fun to explore the limitations of Rust's type theory like this. I had zero ideas you could "cheat" like that with generic types that never overlap, only used to guide Rust's type inference system. Pretty cool!