This is the second in a series of posts about monads (part 1, part 3). I happen to be a .NET developer, so I use C# in my examples, but the concept should apply to any language that has first-class functions. Code for the series is at github.
We left off with the definition of a monad. Let's review:
- Formally, a monad consists of a type constructor M and two operations, bind and return.
- The operations must fulfill several properties to allow the correct composition of monadic functions (i.e. functions that use values from the monad as their arguments or return value).
- In most contexts, a value of type M a can be thought of as an action that returns a value of type a.
- The return operation takes a value from a plain type a and puts it into a monadic container of type M a.
- The bind operation chains a monadic value of type M a with a function of type a → M b to create a monadic value of type M b, effectively creating an action that chooses the next action based on the results of previous actions.
Let's begin by addressing the first bullet point.
Formally, a monad consists of a type constructor M and two operations, bind and return.As I understand it, a type constructor in .NET is analogous to a generic type. And with that little bit of information, we can start writing code. Let's start our project by creating a generic type, Monad<T>.
public class Monad<T> { }github commit
With that, we've satisfied the first half of the first bullet point. We still need to define bind and return, but, since the fourth and fifth bullet points expand on each function, let's declare the first bullet point complete (that was easy!).
Let's move on to the third bullet point (I know I'm skipping around here):
In most contexts, a value of type M a can be thought of as an action that returns a value of type a.I may be taking some liberties with the formal definition, but this screams "PROPERTY!" to me. So let's add a read-only property, Value, of type T, backed by a readonly field of the same type. And let's provide a single constructor whose parameter is of type T. We want to force people to do the right thing - always provide the monad with a value, and never (ever!) change the its value. Our monad follows the spirit of functional programming - it is immutable.
public class Monad<T> { private readonly T _value; public Monad(T value) { _value = value; } public T Value { get { return _value; } } }github commit
As you might expect, usage of our class is very simple, as it does nearly nothing.
var m = new Monad<int>(128); Console.WriteLine("The value of m is {0}.", m.Value);github commit
As boring as the code is - the declaration of class Monad<T> and its usage - we have actually done something quite interesting here. If you think about it, just by having a generic type and a constructor, it fulfills the fourth bullet point:
The return operation takes a value from a plain type a and puts it into a monadic container of type M a.We took a value, 128, and wrapped it in our class, Monad<int>. Again, we've taken some liberties with the formal definition. Instead of a actual return function, we have a constructor. I don't think it's too much of a stretch.
As little as our class does, I'm not exactly happy with it. More specifically, I'm not happy with its usage. If we already have an value that we want to wrap in our monad, why should we have to explicitly provide the generic type argument in the constructor? Why can't C# just figure it out? In short, that's just how constructors work in C#. And you'll just have to get used to it. But we can do better. We can provide a nice, easy-to-use workaround: an extension method.
public static Monad<T> ToMonad<T>(this T value) { return new Monad<T>(value); }github commit
It makes creating an instance of our class much easier on the eyes:
var m = 128.ToMonad(); Console.WriteLine("The value of m is {0}.", m.Value);github commit
That about wraps it up for this post. We have satisfied over half (2 2/3 by my count) of the five conditions for a monad. We have a generic class. It can wrap any value of any type. And it exposes its value as a read-only property. But we don't have a monad yet. So what's left? The bind method, and the ability to chain monadic functions. Stay tuned, things are about to get interesting.
No comments:
Post a Comment