Last time on FAIC I set out to explore monads from an object-oriented programmer’s perspective, rather than delving into the functional programmer’s perspective immediately. The “monad pattern” is a design pattern for types, and a “monad” is a type that uses that pattern. Rather than describing the pattern itself, let’s start by listing some monad-ish types that you are almost certainly very familiar with, and see what they have in common [1. These five types are the ones that immediately come to my mind; I am probably missing some. If you have an example of a commonly-used C# type that is monadic in nature, please leave a comment]:
Nullable<T>— represents a T that could be null[2. As I've discussed before, null in a value type is typically interpreted as "the thing has a value but I don't know what it is". That is, there is a decimal that is the net profits for December, I just don't know what that decimal is right now so I'll say "null". It can also be interpreted as "the thing doesn't even have a value". It's not that we don't know the height of the king of France right now, it's that there is no king of France in the first place, so the height of the king of France is null. The exact semantics are not particularly relevant to our discussion of monadic types however.]
Func<T>— represents a T that can be computed on demand
Lazy<T>— represents a T that can be computed on demand once, then cached
Task<T>— represents a T that is being computed asynchronously and will be available in the future, if it isn’t already
IEnumerable<T>— represents an ordered, read-only sequence of zero or more Ts
So, what do these types have in common? The most obvious thing is that they are generic types with exactly one type parameter. Moreover, these types are embarrassingly generic. With the exception of
Nullable<T>, all of these type work equally well with any T whatsoever; they are totally “agnostic” as to the semantics of their underlying type. And even
Nullable<T> is only restricted to non-nullable value types[3. And even this is essentially an accident of history; it just so happened that when C# was first implemented it had always-nullable reference types, non-nullable value types, and no generic types at all. In a counterfactual world where the CLR had generic types from the get-go, it seems plausible that
Nullable<T> could have been implemented to work on any type, and reference types would then be non-nullable by default. We could have a type system where
Nullable<string> was the only legal way to represent "a string that can be null". Keep this in mind the next time you design a new type system!].
Another way to look at these generic types is that they are “amplifiers”[4. I am indebted to my erstwhile colleague Wes Dyer for this interpretation of monads; his article on monads was a crucial step in my understanding of the concept.] that increase the representational power of their “underlying” type. A byte can be one of 256 values; that’s very useful but also very simple. By using generic types we can represent “an asynchronously-computed sequence of nullable bytes” very easily; that adds a huge amount of power to the “byte” type without changing its fundamental “byte-ish” nature.
So is a monad simply an embarrassingly generic type of one parameter that conceptually “adds power” to its underlying type? Not quite; there are a couple more things we need in order to have an implementation of the “monad pattern”. Next time on FAIC we’ll try to perform some operations on these five types and see if we can suss out any other commonality.