Monads, part three

In this series we're approaching an understanding of monads "bottom up", by trying to suss out the pattern, rather than by going "top down", starting with category theory or functional language practice. Last time I identified five generic types that are frequently used in C#; today we'll start looking for commonalities beyond their being generic types.

Before we get into that though I want to call out three small ways that I'm going to try to increase the clarity of this exposition:

First, one of the types I mentioned last time was the generic delegate type Func<T>, which represents a T that can be produced on demand. The problem is that as we start exploring the higher-order functional programming nature of monads, I'm going to need to use Func<A, R> to represent functions of one variable, and it's going to get confusing to also use Func<T> to represent a T that can be produced on demand. I'm therefore going to from now on assume that we have a generic delegate

delegate T OnDemand<T>();



Third, I'm also going to ignore the fact that the Lazy<T> type has different thread-safety modes you can put it into; deriving a new lazy value from an old one probably should preserve the original value's thread safety mode. I'm going to ignore that issue.

OK, with that out of the way, let's dig in. The first thing to notice about the nullable, lazy, on-demand, asynchronous and sequence types is that they are all things that produce a value, more or less. The nullable type might not produce a value, and the sequence type might produce any number of values, but the values are always values of the underlying type. All these types are in some sense a "wrapper" around some number of values of the underlying type. And in fact, we can go farther than that. If you have a particular value of the underlying type, you can always easily make a wrapper type that produces that value:

{ return new Nullable<T>(item); }
{ return ()=>item; }
{ yield return item; }


And in fact this is the first requirement of the monad pattern: if M<T> is a monadic type then there's got to be an easy way to turn any value of type T into a value of type M<T>.

Now, you might think that of course the second requirement is "and you've got to be able to get the values back out again too". As we'll see, that's close but not quite right. The second requirement is more subtle than that, so we're going to approach it cautiously. We'll start with a very specific question: you can add one to an integer, but how do you "add one" to a monadic wrapper around an integer?

As we just spent eight entire episodes discussing, the compiler already knows how to do so to a nullable int. This is already "baked in" to the language; you just add one and the compiler takes care of everything for you. But if it didn't, we do know how to write the code. I'll write it much less concisely than normal to illustrate each step carefully:

  if (nullable.HasValue)
    int unwrapped = nullable.Value;
    int result = unwrapped + 1;
    return CreateSimpleNullable(result);
    return new Nullable<int>();


  int unwrapped = onDemand();
  int result = unwrapped + 1;
  return CreateSimpleOnDemand(result);



and add one, you do not end up with

()=>DateTime.Now.Seconds + 1

but rather

()=>some fixed value


  return ()=>
    int unwrapped = onDemand();
    int result = unwrapped + 1;
    return result;

(Again, I'm showing all the steps explicitly; realistically we'd write this code with far more concision.)


Now, in the correct code you'll notice that we did use the "unwrap and do the operation" mechanism. The difference between the nullable int operation and the on-demand int operation lies in how the resulting wrapped type is created. With the nullable monad we can get away with aggressively unwrapping the value, doing the computation, and producing a simple wrapper around the new value. The on-demand monad has to be a lot more clever about how the new wrapper is created; it is not a simple wrapper around a value. Rather, it produces an object whose structure encodes the sequence of operations that are going to happen on demand1. In this case, it produces a delegate that invokes the original delegate.

Let's quickly go through the "add one" operations on our other integer wrapper examples. I'll continue to be unnecessarily verbose.

  return new Lazy<int>(()=>
    int unwrapped = lazy.Value;
    int result = unwrapped + 1;
    return result;


  int unwrapped = await task;
  int result = unwrapped + 1;
  return result;

Well that was easy! This looks even easier than the nullable example we started with, but that's because we are taking advantage of the C# 5 compiler's ability to generate a whole lot of code on your behalf. As an exercise, try writing the AddOne operation on asynchronously-computed integers in C# 4.

  foreach(int unwrapped in sequence)
    int result = unwrapped + 1;
    yield return result; 

Again, we are taking advantage of the C# compiler's ability to write lots of code on your behalf. It is instructive to write out this code without using foreach or yield. 2

All right, what have we learned so far?

  • The first rule of the monad pattern is that there's always an easy way to go from a value of the "underlying" type to a value of the "wrapped" type.

That second rule looks like it could use some work. Next time on FAIC we'll start to generalize from our example of adding one to a "wrapped integer" as we continue to refine the second rule of the monad pattern.

  1. This ability is one of the key feature that makes monads useful; we'll return to this point later on in the series.

30 thoughts on “Monads, part three

  1. Excellent post as always, Eric. I've always used Monads in a more intuitive way (i.e. they're just a common pattern), but never really understood them coming from the functional perspective. Your bottom-up approach is starting to really clear them up for me conceptually and I think I can see where this is going, now.

    • I made considerable progress at the end of last year when I had some time off; my last eight castings worked well. But I've gotten busy with a new job, and I can't melt aluminum in the rain. I'll pick it up again in the spring.

      • Cool, looking forward to it! Your blog inspired me to start learning about casting myself, it's something I've wanted to try for a while.




    • Don't see why - you can set breakpoints in lambas, and all the normal VS debugging tools will work correctly in that context (in my experience anyway).

    • It depends. On the one hand, debugging asynchronous workflows can be tricky. You don't have the "call stack" crutch to lean on because it is no longer the case that *where you came from* is logically connected to *where you're going next*. And it is hard to look at a bunch of tasks and understand how they are all related.



      For example:

      With another thread (or in my case many other threads) calling someDelegate(). You can even stick a break point in the lamda and still not see what's going on. But the second one produces the expected results, the first is very unpredictable especially if those other threads are not busy and someDelegate is going into a ConcurrentQueue.

      • Bah it mangled the loop declaration. Just picture a loop with a lot of iterations, the second one with "int j = i" before a lamda declaration.


    I feel the formal way of explaining them is very useful and concise ... for people who already understand them! All others get lot in the details and in the formulae.

  5. "concision" What a wonderful word. Great series already! Looking forward to the rest of the journey - I've read many articles on the "what are they" of monads but never really got to the "why do I care" level of understanding.

  6. I confess I never had much trouble understanding monads, although I've always found the "explanations for OO programmers" to be confusingly verbose (although given the comments on these posts, that makes me an outlier!).

    In the past I have always explained monads like this:


    Now imagine you want to extend this idea in two ways:
    Moreover, you'd like to do this without having to make changes to your "step" functions.

    At this point you might come up with the idea of a "glue" function to join your steps together. Now your processing function looks something like this step -> glue -> (step -> glue -> (...)). The "glue" has the job of updating the meta-data (e.g., incrementing the "number of steps" counter) and/or making a decision (e.g., if the number of steps has reached some threshold, skip the remaining steps).


    Now, if you call your set-up function (which contains the initial input and meta-data) "return" and your glue function "bind", then you pretty much have a monad.

    Because the "return" and "bind" functions all have the same "shape", you can construct families of operations over monads which do useful things for ALL monads, all without having to change your original "step" functions.

  7. Pingback: Weekly Digest 3 | chodounsky

    Does the pattern indicate whether there should be side effects? (Coming from functional programming, it seems as if it should not). E.g., wrapping a Lazy, given that the monadic pattern as you indicated should encode the *sequence of operations*, it seems as if the operations for initializing the original Lazy shouldn't initialize the original Lazy, but in fact should be encoded in the new Lazy's initialization and the original should be unaffected.

    • The intersection of monads, side effects and functional languages is precisely what motivates their use in those languages: monads are used in purely-functional languages to encode the notion of "these side effects must occur in this order", even though such languages discourage side effects and often do out-of-order evaluation.

  9. (Disclaimer: I'm not familiar with C#, I am familiar with Haskell, and I am unfamiliar with and eschew Category theory.)


    The property is described as: "we unwrap it, then we perform the addition operation on the unwrapped value, and then we wrap the resulting value."

    If we replace "addition operation" with "an operation", then whether or not we are describing a Functor or Monad depends on whether that operation knows about the wrapper or not.

    In the case of adding one (where we think of the + operator as a two argument function), it's arguments and return do not involve the wrapping type, so this is a Functor property.


    I hope this clarification helps. IMO, it's helpful to introduce Functors first (and all of these examples actually do so!), and then proceed to Monads. (There is also a common middle step called Applicative.)

    • That is precisely what I am attempting to do -- introduce functors and then procede to monads -- but I am attempting to avoid saying "functor" any more than necessary because I'm trying to keep this friendly to developers who don't have a background in functional programming, algebra, etc. Thanks for the note!

  10. I'm doing some code testing and I noticed the method
    async static Task AddOne(Task task)
    int unwrapped = await task;
    int result = unwrapped + 1;
    return result;

    • Good point; the question of whether tasks are "hot" -- started as soon as they are created -- or "cold" -- started explicitly -- was a long debate when we were designing async/await. I'm glossing over those kinds of details in this series.

  11. I'm sure I must be doing something stupid, but I can't figure out what. The "CreateSimpleNullable" function refuses to compile for me, on both VS2010 and VS2012. The section "return new Nullable(item);" gives an error on the which reads "The type 'T' must be a non-nullable value type in order to use it as parameter 'T' in the generic type or method 'System.Nullable'

    • Gaah. Didn't realise the comments would strip of angle brackets and any contents. The two incorrect bits should read:
      return new Nullable<T>(item)
      gives an error on the <T> which reads

      • Nullable (of T) has a constraint on T that says that it must be a "struct" (i.e. a value type). Somewhere in the introduction to all this, Eric pointed out that he was going to ignore that constraint in his discussions. If you want to get things to compile, you will need to constrain T properly.

Leave a Reply

