Crate coyoneda [−] [src]
Functor composition via the Co-Yoneda Lemma
Functors in Rust
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.
The Co-Yoneda Lemma
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
.
Composing Coyoneda
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!
Structs
Coyoneda |