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.
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
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.
Aside: This is essentially an accident of history. 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. 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”. (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.) An “amplifier” is something that increases 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:
Task<IEnumerable<Nullable<byte>>>. 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.