Let's implement functors in Rust!

Working around the lack of higher-kinded types, our trait for a functor will look something like this:

pub trait Param { type Param ; } pub trait ReParam < B > : Param { type Output : Param < Param = B > ; } pub trait Functor < 'a , B > : ReParam < B > { fn fmap < F : Fn ( Self :: Param ) -> B + 'a > ( self , F ) -> Self :: Output ; }

This works great as long as we write functions that take specific types which are functors, but it is not possible to write a function operating on a generic functor type and using fmap more than once. For example, the following will not compile:

fn add_and_to_string < 'a , F > ( x : F ) -> < F as ReParam < String >> :: Output where F : Param < Param = i32 > + Functor < 'a , i32 > + Functor < 'a , String > { x . fmap ( | n : i32 | n + 1 ) . fmap ( | n : i32 | n . to_string ()) }

While functors in general can be encoded to some extend in Rust's trait system, what we can't encode for a lack of higher-kinded types, is the fact that a functor Box maps a function between A and B to a function between Box<A> and Box<B> , not between Box<A> and Option<B> .

Especially when looking at functor composition, it is useful to be able to encode this fact, because it allows us to chain multiple calls to fmap , knowing that the result is also a functor, and can be fmap 'ed further.

Let's define a data type called Coyoneda :

pub struct Coyoneda < 'a , T : Param , B > { point : T , morph : Fn ( T :: Param ) -> B + 'a }

This datatype is a functor, which uses function composition to accumulate the mapping function, without changing the captured T . The implementation for Functor is trivial:

impl < 'a , T : Param , B , C > Functor < 'a , C > for Coyoneda < 'a , T , B > { type Output = Coyoneda < 'a , T , C > ; fn fmap < F : Fn ( B ) -> C + 'a > ( self , f : F ) -> Coyoneda < 'a , T , C > { let g = self . morph ; Coyoneda { point : self . point , morph : move | x | f ( g ( x ))} } }

The co-yoneda lemma states that for a covariant functor f , this Coyoneda f is naturally isomorphic to f . Practically speaking, this means that we can lift any f a into a Coyoneda f a , and given a function (a -> b) -> f b , we can retrieve back a f b from a Coyoneda f b .

Now here's the catch: Since we have a parameterized datatype that is isomorphic to any functor, we can lift functors into Coyoneda to compose them safely within Rust's type system!

For example, let's implement a function that is generic for any functor, by operating on our Coyoneda type:

fn add_and_to_string < T : Param > ( y : Coyoneda < T , i32 > ) -> Coyoneda < T , String > { y . fmap ( | n : i32 | n + 1 ) . fmap ( | n : i32 | n . to_string ()) }

Given we implemented a functor for Option , we can now do the following:

let y = add_and_to_string ( From :: from ( Some ( 42 ))); assert_eq ! ( y . unwrap (), Some ( "43" . to_string ()))

... or for Box :

let y = add_and_to_string ( From :: from ( Box :: new ( 42 ))); assert_eq ! ( y . unwrap (), Box :: new ( "43" . to_string ()))

... and for every other functor as well. Yay!