Monad Interface: Rust Edition

Implementing Monads in Rust

Ross
Ross
Nov 29, 2020 · 7 min read
Photo by Zane Lee on Unsplash

I’ve done a lot of writing about functional programming. But mostly in JavaScript. I love the simplicity of declarations that are possible thanks to JavaScript’s dynamic typing system. It makes defining generic things very easy — a concept that is much more complicated in strongly typed languages. One generic concept that I love expressing in JS is the functional keystone known as the Monad. A Monad is an encapsulation of an associative binary operation. In other words, you can call map on it with an appropriate function parameter to change the inner value — even its type. It wraps some value and and allows us to adhere to a simple interface to act on that value. So how can I express this highly generic concept in Rust? Is it even possible?

I assume, reader, that you have some interest in functional programming. You may even already know what a Monad is, and simply want to know how to implement them in Rust. If this is you, read on! You’ve come to the right place.

Recently, I was looking for examples of good implementations of Monad in Rust and I came across this article. Its all about using generic associated types in traits. I was reading along, eagerly eating up the information. This is beautiful, I thought, this is amazing, this— isn’t currently possible in Rust. Unfortunately, at the time of writing this article using generics in associated types is unstable. Even enabling the experimental feature isn’t guaranteed to work (and largely, doesn’t). But that’s OK. I decided I’d just write my own implementation instead.

Pointy Structs

Firstly, Monads tend to adhere to an interface known as Pointed:

The Pointed interface says, ‘I am a wrapper type that can wrap and unwrap a value T.’ It has an associated type, Unit. This represents the type that get’s encapsulated by the implementer. of is the constructor of the wrapper. unwrap removes the wrapper, returning the encapsulated unit. Notice another thing about this trait definition — we make use of a where clause. where clauses are powerful in trait definitions. They can be used to ensure that certain methods and values are available to you within the trait definition. They can restrict the types that can implement this trait as well. In our clause, we declare Self as Sized. Declaring Self as Sized means that this trait can only be implemented by structures that can have a statically known size.

Now, let’s write a simple struct to implement this Trait:

Identity is one of the simplest Monads to understand. It takes any value and wraps it up with no special behavior. We can define our implementation generically using impl<T>. Now we’ve implemented Pointed for any T of Identity.

However Identity isn’t nearly a Monad yet, and we can’t even operate on it’s inner value. It’s still a few big steps behind being monadic.

Funky Functors

Additional to their adherence to the Pointed interface, Monads are also Functors. Functors extend what Pointed defines. Not only can a Functor hold a value, but it can operate on that value without removing it from the box. This is thanks to the concept of map. map allows us to apply a function to the value that is hiding within our box. How can we define it?

By using the sub-trait syntax (Trait : SubTrait), we can declare that everything we defined in the sub-trait needs to be implemented in this trait. So that means that Functor intrinsically has of and unwrap as part of its interface. The main part of Functor, however, is map. map takes a function F and returns a Functor B. What that means is that the function you give to map might transform the type that you’re working with. So you may start with an Identity<u32> and you might transform it somehow into an Identity<&str>.

Check out our where clause in this block. This one is local to the map function. I love how easy trait bounds are to read. B: Functor almost reads as the plain English, ‘B is a Functor’. In order to accommodate a default functionality without knowing the types, I rely on the things defined by my trait bounds like Pointed’s Self::Unit associated type and of function. Conveniently with this implementation, the type parameters can usually be elided (meaning you don’t have to declare them at the call site).

Let’s implement it for Identity:

Since our default implementation does nothing special with the values, we can reuse it for Identity without declaring any unique definition. How nice is that? Let’s give it a test run:

This should produce Identity(5) and Identity(15). Easy.

Full Blown Monads

Now that we have a Functor, let’s go ahead and make a full fledged Monad out of this. We’ll define another trait:

Monad compounds both Functor and Pointed. This means it can lift a value into context with of, and operate on that value without removing it from the box using map. And now we will be able to do something else — chain. chain lets us take a function that would normally return a Monad and map it into another Monad. Now, normally if we were to do this using map, we’d end up with an Identity<Identity<T>>. But we don’t ever really want that, we just want Identity<T>. This is where chain comes in handy. It does the lifting function, but discards the outer box. Our generic implementation applies the lift to the unwrapped value of the supplied self Monad. Since we have this nice default implementation we can alsofcb implement Monad as a one-liner for Identity:

Let’s test this to make sure everything is working alright:

That should spit out Identity(5), Identity(15), Identity(12) to the console. Woohoo, it works! Although I wish I didn’t have that pesky type declaration on id2. Try removing it. You’ll get a type annotation needed error from the compiler, unfortunately. I like type elision when I can get it.

Does It Work?

Let’s try to define a Monad that’s a bit more complicated than Identity. We’ll still stay pretty simple for this example. Although tuples are already supported by Rust, let’s make a dedicated Pair type.

And we will of course have to implement some traits, starting with Pointed:

Simple enough, that definition allows us to easily create Pairs and convert them back into native tuples. Unit controls what we get to play with in our map function — in this case the tuple type (A, B).

We can one-line our Functor and Monad implementations, as with Identity:

And now we can play some more. Let’s test out each of Pair’s Monad Interface functions:

When this program runs, it should spit out Pair(1, 2), Pair(2, 5), and Pair(5, 2). Our constructor gives us back just what we’ve lifted into the monadic context. Our map gives us a transformed Pair. And finally, our chain constructs a flipped version of our Pair. Look out, it’s Functional Rust in action!

Heres a link to the Playground.

Conclusion

I hope you’ve enjoyed another expedition into the wilderness that is Functional Rust. In the Rust world, where everything is statically typed and memory safe, the compiler makes a lot of demands from us as the developers at compile time. As a guy coming from JS, this makes it difficult for me to write what I feel is truly flexible and generic code at times. But the fact that any of this is possible in a systems language is very impressive. High level abstractions like these are generally reserved for productivity languages. Rust is a diamond in the rough in that it offers us free abstraction via Traits and generic parameters while preserving the surgical accuracy and performance expected from a systems language. If you’re not interested now, check on Rust again in a couple years. Rust will be everywhere.

The Startup

Get smarter at building your thing. Join The Startup’s +792K followers.

Sign up for Top 10 Stories

By The Startup

Get smarter at building your thing. Subscribe to receive The Startup's top 10 most read stories — delivered straight into your inbox, once a week. Take a look.

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Ross

Written by

Ross

Programming maniac, #JavaScript zealot. I'm crazy about #FunctionalProgramming and I love Rust.

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +792K followers.

Ross

Written by

Ross

Programming maniac, #JavaScript zealot. I'm crazy about #FunctionalProgramming and I love Rust.

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +792K followers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store