I finally put in some time and effort learning myself a bit of Rust. Though I am still struggling with ownership and lifetimes (which is essentially everything about the language, to be honest), I find it more interesting compared to Golang, which is relatively boring, though being functional (no pun intended). While learning the language, the one thing I came across often is the Option
enum, then I remembered that I read something about Monad.
It was a great Aha moment!
So while I am getting used to seeing compiling errors due to lifetimes and ownership Option
enum, namely Some(value)
and None
, I felt like I found something that somehow answers my frustration not answered by typed languages. There's one thing I like from dynamic-typed language that is not possible in typed language like Golang, which is what to return when the user send in rubbish to the function. Of course, sending a zero value is usually what I assume most people do, but I prefer sending something back in different type to differentiate between "Hey you are sending me nonsense" and "Hey the result is zero".
Rust answers this through the use of enum, and one of the most used is provided in standard library which is Option
. Then I realized in a lot of places there are being used, so I started reading more and experiment with them. So after getting used to the enum, revisiting the article gave me a new insight into understanding Monad. I didn't really understand what the post was about before this. Category theory in total was so abstract I didn't get a thing, even reading / watching numerous talk and tutorial posts.
But then I now know slightly more.
And I thought if it is possible to somehow do it in Rust, by cheating a little bit, as the original article was written for Haskell programmers. Then I came out with something like this (the code is of proof-of-concept quality, and probably violates all the ownership rules, obviously).
First I started by implementing Functor. Functor is a wrapped value that can be fmapped by a function. In the land of Rustlang, we can probably represent this as a trait, as follows,
pub trait Functor<T, U, V> {
fn fmap(self, func: fn(T) -> U) -> V;
}
So fmaps takes in a functor, and a function that takes an unwrapped value, and return another unwrapped value. After some computation, fmap returns a wrapped value. The analogy of wrapped value is usually used when describing Monads, Applicatives and Functors, which is practically putting values into something that wraps around it, perhaps a box. The type of wrapped value that made this post is Option
, but there are other types too, for instance a list of values in the form of Vectors, or even another function/anonymous function/closure. Therefore, we have the following implementations for the trait above, in the mentioned three forms
impl Functor<i32, i32, Option<i32>> for Option<i32> {
fn fmap(self, func: fn(i32) -> i32) -> Option<i32> {
self.map(func)
}
}
impl Functor<i32, i32, Vec<i32>> for Vec<i32> {
fn fmap(self, func: fn(i32) -> i32) -> Vec<i32> {
self.into_iter().map(|x| func(x)).collect()
}
}
impl<F: 'static> Functor<i32, i32, Box<Fn(i32) -> i32>> for F where F: Fn(i32) -> i32 {
fn fmap(self, func: fn(i32) -> i32) -> Box<Fn(i32) -> i32> {
Box::new(move |x| {
func(self(x))
})
}
}
This is exactly what I was referring to as cheating. The function body makes it really clear that fmap is already implemented in a different way in Rust, and I am just rewiring them in different way. But I am doing this to further understand what those things are, so I guess it is ... fine? One thing I am not sure about the code is that I probably should borrow stuff instead of moving them into the function, but I gave up wrestling the compiler, and most importantly, they work, somehow.
// Returns Some(2)
println!("Functor (wrapped value) {:?}", Some(1).fmap(|x| x * 2));
// Returns vec![2, 4, 6]
println!("Functor (vec) {:?}", vec![1, 2, 3].fmap(|x| x * 2));
// Returns 6
println!("Functor (function composition) {:?}", (|x| x + 1).fmap(|x| x * 2)(2));
Then I went on with Monad. Monad on the other hand is something that implements a function called bind.
pub trait Monad<T, U> {
fn bind(self, func: fn(T) -> U) -> U;
}
Unlike fmap, the function required by bind takes an unwrapped value, and return a wrapped value. Bind itself returns a wrapped value. So cheating away
impl Monad<i32, Option<i32>> for Option<i32> {
fn bind(self, func: fn(i32) -> Option<i32>) -> Option<i32> {
self.and_then(func)
}
}
I only do for Option<i32>
this time as I got a bit tired wrestling with the compiler errors, also I already kinda get what it is by the time this was done. Laziness is also why I skipped Applicatives, partly also because I am not that comfortable enough with Rust to reimplement lift, which is the function that defines an Applicative. Perhaps when I get more comfortable with Rust I would attempt again (and possibly re-implement Maybe).
So I shall stop here and continue with my work.
Top comments (4)
Hey,
There's an important concept in Haskell called Higher-kinded Kinded polymorphism, which is at the moment missing in Rust. Unfortunately, without this you can't really express the essence of these concepts.
The functor itself should be polymorphic on type which is itself polymorphic, e.g.:
Functor<F<A>>
In here the
F
is our higher-kinded type polymorphic on another typeA
(btw, in Haskell a type of types is called kind).And then your functor definition would look more like this (this is not valid Rust code):
In here we're applying a function
A -> B
over aF<A>
to get back aF<B>
.And then the instance for Option would look more like this:
Here, the
Option
steps in as a concrete implementation ofF
from the definition ofFunctor
above, whileA
stays polymorphic. So we're applying a functionA -> B
over anOption<A>
to get back anOption<B>
.also I kinda miss manipulating list/string like I did in clojure in Rust too lol, but in Rust the preferred way seem to be doing everything in place, rather than returning a new list/string whenever mutation happens.
I know, I only implement it for i32 because I wanted to get a feeling of what they are (Functor, Applicatives, Monad), not trying to implement them in Rust actually (hence this is a cheat as it is not a full implementation).
Also I am still learning Rust (which is surprisingly hard when it comes to ownership, liftimes etc.) and is still struggling lol. Am currently going through the Rust-101 tutorial. I previously picked up a language through a random list of coding puzzles/katas (for example 4clojure when I was learning clojure), but i don't really learn much about the language completing those problems, so preferred something more structured. Rust-101 is close, but am still looking for something better.
I see. It’s been a while since touched Rust, so I can’t comment much on it, but you might like the immutable collections crate immutable.rs/