Associated traits will bring Rust 1 step closer to having higher-kinded types

The Functor trait is an abstraction for the map function which is commonly seen on Option::map, Iterator::map, Array::map.

impl Functor for Collection would mean you can get from a Collection<A> to Collection<B> by calling map and supplying a closure with type A -> B.

Functor requires higher-kinded types, as you cannot implement traits on collections directly. However, we can think of a workaround by using generic associated types:

trait Functor<A> {
    type Collection<T>;

    fn fmap<B, F: Fn(A) -> B>(self, f: F) -> Self::Collection<B>;
}

In the above, the A in Functor<A> represents the input, and B represents the output. Collection<T> is a generic associated type representing the collection.

Here’s how you could implement it for a Vec<T> in stable Rust today:

impl<A> Functor<A> for Vec<A> {
    type Collection<T> = Vec<T>;

    fn fmap<B, F: Fn(A) -> B>(self, f: F) -> Self::Collection<B> {
        self.into_iter().map(f).collect()
    }
}

It works for Vec<T>, but if you try to implement it for a HashSet<T>, you’ll run into a problem:

impl<A: Hash + Eq> Functor<A> for HashSet<A> {
    type Collection<T> = HashSet<T>;

    fn fmap<B, F: Fn(A) -> B>(self, f: F) -> Self::Collection<B> {
        self.into_iter().map(f).collect()
    }
}

In order for the above code to compile, B needs to be Hash + Eq as HashSet<T> where T: Hash + Eq.

If you try to add this constraint to B, you won’t be able to because the signature of fmap in the trait definition and impl for HashSet will be mismatched:

// trait Functor<A>
fn fmap<B, F: Fn(A) -> B>(self, f: F) -> Self::Collection<B>

// impl<A: Hash + Eq> Functor<A> for HashSet<A>
fn fmap<B: Hash + Eq, F: Fn(A) -> B>(self, f: F) -> Self::Collection<B>

How do this solve this? Creating the Functor trait in today’s rust is not possible due to the above limitations. Rust does not have “higher-kinded” types.

However, with a natural extension to the language we can think about the “associated trait” feature that would fit into Rust.

This feature will allow you to write a trait bound inside of a trait, which the implementor will need to fill. It is similar to the “associated types”.

With associated traits, we can define the Functor trait as follows:

trait Functor<A> {
    type Collection<T>;
    trait Constraint;

    fn fmap<B: Self::Constraint, F: Fn(A) -> B>(self, f: F) -> Self::Collection<B>;
}

In the above, we declare a trait Constraint which will need to be provided by the implementor of the caller.

The generic B now must satisfy the bound. B: Self::Constraint. This allows us to implement Functor for HashSet:

impl<A: Hash + Eq> Functor<A> for HashSet<A> {
    type Collection<T> = HashSet<T>;
    trait Constraint = Hash + Eq;

    fn fmap<B: Self::Constraint, F: Fn(A) -> B>(self, f: F) -> Self::Collection<B> {
        self.into_iter().map(f).collect()
    }
}

Both A: Hash + Eq and B: Hash + Eq, so this code will compile.

The impl Functor for Vec<T> does not need to provide any constraint, but it must be included. In Vec<T> the T has no trait constraints. How do we work around this?

Define a AnyType trait which is implemented for all types:

trait AnyType {}
impl<T> AnyType for T

This allows us to implement Functor for Vec again:

impl<A> Functor<A> for Vec<A> {
    type Collection<T> = Vec<T>;
    trait Constraint = AnyType;

    fn fmap<B: Self::Constraint, F: Fn(A) -> B>(self, f: F) -> Self::Collection<B> {
        self.into_iter().map(f).collect()
    }
}

Because the AnyType associated trait is implemented for all types, Vec<T> where T: AnyType is identical to Vec<T>. It is a bit wordier, but it gives us the flexibility of implementing the Functor trait for any type.

Effectively, this gives us higher-kinded types in Rust. When you see impl<A> Functor<A> for Vec<A>, Vec<A> is the output type.

If you want to learn more about this feature, as well as extra use-cases, check out the issue! There is currently no RFC to add it.