I made a big claim in part 1, that Monads are “an extraordinarily powerful concept”, without in any way backing it up. So before going on to explain what Monads are and how they work, I should attempt to define the problem they are trying to solve. The difficulty is that they are a kind of ‘meta solution’ to a whole range of programming problems. It’s difficult to explain the problem they are trying to solve without being, on the one hand, too specific and leaving the reader with the impression that they only solve that one problem, or, on the other, so general that it just sounds like a load of mysterious hand waving. I’m going to go for the later now, in the hope that I can bring us back down to earth with some specific examples later in the series.
A Monad is a pattern for doing function composition with ‘amplified’ types. You can think of an amplified type as a generic type with a single type parameter. IEnumerable<T> is a very good example. Monads provide techniques for removing repetitive and awkward code and can allow us to significantly simplify many programming problems. Historically Monads emerged from a branch of mathematics, Category Theory. They were first used in computing by the designers of the Haskell programming language to elegantly introduce side-effecting functions into the ‘pure’ Haskell type system. You don’t need to understand Category Theory (I certainly don’t), or Haskell to understand and use Monads.
Usually, when we program, we manipulate values. These can be simple types, built into the language, like ‘int’, ‘bool’ or ‘string’, or more complex types we define ourselves, like ‘Customer’. We pass these around and do various things with them. The nicest kind of programming is when our programming language lets us express nicely what we are hoping to achieve:
bool IsGreaterThanTen(int value)
{
return value > 10;
}
We can use this really easily:
if(IsGreaterThanTen(x)) {
// do something
}
But what if our function throws an exception?
bool IsAFactorOfTwelve(int value)
{
if(value == 0) throw new Exception("Can't divide by zero");
return (12 % value) == 0;
}
Now we have to surround our use of this function with some boiler-plate code; a try-catch block:
try
{
if(IsAFactorOfTwelve(x))
{
// do something
}
}
catch(Exception e)
{
// handle the exception
}
What if our function could return null? (yes, I know these examples are pretty silly, but you get the point)
string Trim(string value)
{
if (value == null) return null;
return value.Trim();
}
Now we have to insert some more boiler-plate to check for null:
var trimmedValue = Trim(inputValue);
if (trimmedValue == null)
{
// so something different here
}
Every time we have to insert boiler-plate code we obscure what we are really trying to achieve. This make our programmes harder to write, harder to read and gives us plenty of opportunities for creating bugs. Boiler-plate is any code that is not the declaration of what we are trying to achieve. It’s the try-catch blocks, the null checks, the iterations. It’s the stuff that makes us violate DRY.
Here’s a more revealing example. what about dealing with lists or collections (if we didn’t have LINQ or its associated extension methods - there’s a very good reason I’m ruling out using these ;) )? Any time we’re faced with an IEnumerable<T> or an IList<T> we shove in a foreach loop:
foreach (var number in Range(5, 10))
{
// do something with the number
}
That’s more boiler plate.
IEnumerable<int> is interesting. Of course it’s a generic type, but we can also think of it as an ‘amplified’ type. We take an int and turn it into a super int, one that, rather than representing a single integer value, represents a whole load of them.
We can represent other things with ‘amplified types’. In the Trim example above we could have returned a Nullable<string> (except we can’t of course because Nullable<T> can only be used with value types), we would have been amplifying string to say that, as well as being a string, it can also be not-a-string, but null. Of course this is implicit for all reference types in C#, which is a shame, it would be nicer if we could decide if we wanted nullability or not.
We could also imagine that our IsAFactorOfTwelve function could have returned an IMightThrowAnException<bool>. It would certainly have been more explicit and intention revealing. It would have returned a super-bool, one that’s a bool value, but may alternatively be an exception. Of course in C# all functions have an implicit Exception return type, this is also a shame, since C# function signatures tell us nothing about any exceptions they might throw.
Monads give us a way of dealing with these ‘amplified types’ and allow us to remove the boiler plate. They can help us write declarative programs where what we are trying to achieve is not obscured by the apparatus we need to achieve it. They help with our core problem as programmers, getting complexity under control so that we can reason effectively about our code.
In part 3 we’ll meet some real Monads and get them to do some simple tricks. Stay tuned.
I'm looking forward to seeing some monad insiprations from you.
ReplyDeleteAnyway, I remember that not including exceptions in function signatures was an explicit decision of c# authors. Unlike the Java platform, this allows for less breaking changes in library code. Personally I like not being forced to mention all exception types that my function might throw. Would I also be responsible for all those exceptions that my function is dependent when using other functions/modules/libraries ?
There's obviously a place for unexpected exceptions. How can you predict when an out-of-memory condition will occur, for example. But when you branch in your own code and throw an exception, I personally think it would be better if the caller has some indication that there is an alternative 'return value'. I don't like the Java implementation either, but the way a lot of Haskell code has an explicit error result is very nice.
ReplyDelete