Yes, I'm writing about monads but fear not; I won't be delving into the depths of category theory. Instead this post shall attempt to remain practical, showing that in practice monads don't have to complex and difficult to understand and actually can be derived simply through refactoring—they're just another abstraction.
Right, let's get started. Imagine that we're building some kind of project component, for manipulating project files which contain lists of files from various sub-directories. Something there could be in an IDE. One might find the following interfaces:
type ProjectFile(path:string) = ... module Project find :: string -> string option load :: string -> ProjectFile option
ProjectFile for encapsulating properties and methods of project files and two functions in the
Project.find to recursively ascend from the current directory until it finds a file with the right extension and
Project.load to parse and load a project file, given its path. Creating and using a project would thus look something like the following:
let doSomething path = let projectPath = Project.find path match projectPath with | None -> None | Some p -> let projectFile = Project.load p match projectFile with | None -> None | Some f -> f.listFiles()
Eeek! That's not very clean, is it? The intuitive refactoring would be to extract the method out like so:
let loadProject path = match Project.find path with | None -> None | Some p -> Project.load p let doSomething path = match loadProject path with | None -> None | Some prj -> prj.listFiles()
This isn't awful but it would have to be redefined for any other cases of nested matches too, which isn't perfect either. The repeated
| None -> None matches don't look very nice either, it would be ideal if they could just be ignored. What would be ideal is a function that ignores
None and passes through any values that do exist; something like this:
let ifSome opt f = match opt with | Some s -> f s | _ -> None let doSomething path = ifSome (Project.find path) (fun p -> ifSome (Project.load p) (fun prj -> prj.listFiles()))
Well, that's gotten rid of those matches at least but it's not exactly elegant. Let's swap out that prefix-positioned function call for an infix operator:
let (>>=) opt f = match opt with | Some s -> f s | _ -> None let doSomething path = Project.find path >>= Project.load >>= (fun prj -> prj.listFiles())
Eureka! Now that's much better. I'm sure you're looking for where monads start coming in to this? Well, I just used them. Option is a monad and
>>= is one of the two monadic operators, called bind. The signature of which, for any monad type
m a -> (a -> m b) -> m b The other operator is return which takes an ordinary value and wraps that value in the monad (signature
a -> m a), for
Option this is, of course, just the
Some constructor. These two operators can be defined for any monad and used in very similar ways thus working with them can translate to any domain or data structure. So, you see, dear reader, monads don't have to be hard; bind especially can be really useful in removing nested stuctures or calls.
There are actually a few implementations of these operations in the core library, the
seq computation expression builders define a number of methods including
Return. In fact computation expressions are essentially syntactic sugar on top of some sort of monads. You can see the operators that are used to build computation expressions and a little more information on how they work here. Although, as demonstrated above, they don't really produce very clean code when used in a prefix position. Aliasing them to operators or just using the computation expressions is likely a good idea in most cases.