\ Note July 6th 2025: this post’s original title was “A list is a monad”. It has been changed to “List is a monad”.The term “monad” is often invoked when describing patterns in functional programming. At the heart of monadic programming is sequencing computations, so each step can depend on the previous one while the monad threads context.You may erroneously think all monads are containers, or burritos, or boxes. The of monads can be idealized as a (albeit a flawed metaphor). Monads are much more than just containers, and there isn’t the-one-and-only monad; instead it’s better to think about them as a programming pattern, recipe, factoring out control flow, or, in some cases, a deferred computation. It depends on which monad you’re talking about.From a teaching perspective, to get the concept for what a monad is, we will start with the simplest of monads which will feel a lot like just a container but with some composable aspects. This provides the infrastructure to understand more complex monads later on.List: Map & flatMap in PracticeTo an OOP developer, monadic types () might look just like generics. It’s a typical pitfall to think “we have generics, so we have monads,” which isn’t true by itself. Monads do usually involve generic types, but they require specific operations ( and ) and the three monad laws on those types to ensure uniform behavior. and is fundamental to working with monads.A good example of a monad is . You’re likely very familiar with lists and working with lists.The monad operation is responsible for: For , runs (a function) on element. For example, let’s define as . The list becomes . If the list doesn’t have any elements, then doesn’t call . doesn’t need to worry about that. Also, doesn’t care if it’s ; all is, is just . is responsible for running it.Managing sequencing and combination. The list context concatenates all results into one list ( does flatten any nested lists, is responsible for this). We don’t need to manually re-add elements via or otherwise manage the collection ourselves.Notice that the monad in this case is responsible for running . This shift means your business logic stays and , you describe happens to a single value, and the monad describes and it happens.This is different from object-oriented and procedural programming because in those paradigms, if you want to process data, it is your responsibility to understand how to apply the function to your data. We have to use different control constructs to handle different types of data, and we’re also responsible for the “how”:public string f(string input) { return input + “ -appended text“; } // 1. List: you must foreach and build a new list var fruits = new List { “apple”, “banana”, “cherry” }; var newFruits = new List(); foreach (var fruit in fruits) { newFruits.Add(f(fruit)); } // 2. Single string: you must check for null first, then concatenate string userInput = GetUserInput(); // could be null if (userInput != null) { userInput = f(userInput); } // userInput could still be null here, or it could be the concatenated result // 3. Dictionary<string, string>: you must know it’s key/value pairs var dict = new Dictionary<string, string> { [“a”] = “alpha”, [“b”] = “beta”, [“c”] = “gamma” }; // can’t modify while iterating, so capture keys first foreach (var key in dict.Keys.ToList()) { dict[key] = f(dict[key]); } In these examples, we are forced to know to update each structure procedurally. For a , we have to call ; for the we can update it in place; for the , we have to iterate over keys and update each entry. We have to know it’s a beforehand to know to use . We have to know it’s just a to append another string to it.With monads, you delegate the control flow to the monad itself, the monad knows how to update its underlying value(s). Recall that even the simplest monads must implement two methods to be monads ( and ) and must follow three monad laws. moves a raw value into the monadic context (this operation is sometimes called “lifting”, “identity”, “return”, “wrap”, or “promotion”, and in some libraries has names like or ).In the list monad, takes a single element and returns a list containing that element.For example, given the integer , produces a list as follows:var list = new List { 1 }; implements because it allows moving a value into the mondaic context. Nothing about the value changes, it’s simply wrapped in a . If you access element of that list, you get back . That’s it. applies a function to each value inside the monad.In , runs a function on every element and outputs a new list with that function applied to each element. Don’t overcomplicate it. For example, suppose we have a function that adds one, . Passing this function to would simply add one to each element in the list. The list would become .var originalList = new List { 0, 1, 2, 3, 4 }; var mapped = originalList.Map(x => x + 1); // Map doesn’t exist in C# (use LINQ’s Select), but assume this pseudocode Example (C#, without monads):var originalList = new List { 0, 1, 2, 3, 4 }; var mappedList = new List(); foreach (int x in originalList) { mappedList.Add(x + 1); } How do you get the damn values out of the monads?Ideally, you don’t want to pull the values out of a monad unless you absolutely have to. It’s possible to implement a method that returns the underlying value, but once the value leaves the monadic context, we lose the benefits of that context and can no longer compose operations easily.Think about as if you had never seen it before. You might say, “I don’t want my values trapped in this list, how am I supposed to use them?” and then manually extract each element into separate variables:// Pretend it’s your first time with List var numbers = new List { 1, 2, 3 }; // — Manual extraction (values “trapped” in the list) — var a = numbers[0]; var b = numbers[1]; var c = numbers[2]; // Now call your function separately on each: var r1 = AddOne(a); var r2 = AddOne(b); var r3 = AddOne(c); But by doing so, you lose the advantages of using a list in the first place: the ability to store arbitrarily long sequences, to pass around all the values together, to concatenate with other lists, and to iterate easily. If you want to add one to each item, extracting them one by one and handling each separately is tedious and error-prone.Up to this point, monads might just seem like “fancy containers” that have to implement two odd methods ( and ). Let’s explore a slightly more complex monad to see why they’re more than just containers.Let’s consider a case where unwrapping the value may not always make sense. We’ll create a monad called (often also called an ) which represents either an existing value or the absence of a value.For simplicity, our will hold an internally (in a real library this would be a generic ). It’s not exactly a full monad yet, because we haven’t implemented on it.public class MaybeMonad { private int value; private bool hasValue; // Unit public MaybeMonad(int value) { this.value = value; this.hasValue = true; } // Unit (no value) public MaybeMonad() { // hasValue remains false by default } // Map public MaybeMonad Map(Func<int, int> func) { if (hasValue) { return new MaybeMonad(func(value)); } return this; } } Here, the operation corresponds to calling one of the constructors, that’s how we lift a raw value into a . The operation might feel a bit strange because we’re just dealing with a single value (or none), whereas you might be used to mapping over a list of many values.For example, to add 1 to a :var age = new MaybeMonad(30); var newAge = age.Map(x => x + 1); // newAge now holds 31 Or if there was no value to begin with:var age = new MaybeMonad(); var newAge = age.Map(x => x + 1); // newAge is still “nothing”, Map didn’t call f(x) because there was no value This looks verbose just to add 1 to a number. Why wrap in a and call when we could have just incremented directly? The point is that is a , by definition it might or might not contain a value. In the case where there is no value, ’s simply does nothing. You’d have to write the same conditional logic yourself in a procedural style:int? age = null; if (age != null) age++; int? age = 30; if (age != null) age++; Now we start to see why a monad is not simply a container to be unwrapped at will. How would you “unwrap” a ? If it has a value, you could return it, sure. But if it doesn’t, there’s nothing to return, the absence itself is a meaningful state. essentially encodes the idea of “nothing” (no result) in a way that isn’t just (because in many languages is still a concrete value of sorts). With , if there’s no value, any function passed into simply won’t execute. Unwrapping it and getting a raw value out isn’t always meaningful in this context.Another benefit of monads is that you can chain computations that themselves produce monadic results. The limitation of only having is that you might end up with nested monads. For example, imagine a function that returns a . If you call on a with that function, the result would be a , a nested container, because the wraps the function’s result into yet another . We need a way to apply a function that returns a monad and avoid this unnecessary nesting when chaining operations. is like our , but it also flattens the result. provides the ability to chain computations that themselves produce monadic values, which is the defining feature of monads. For example, if you have a function that looks up a user and returns a , but you want to pass it to another function that returns the user’s profile. Using would give you a Maybe<Maybe>, an awkward nested container because the input would be a . With , you both apply your lookup and collapse the layers in one go, so you can seamlessly sequence optional, error-handling, or asynchronous operations (e.g. promises/tasks) without ever wrestling with nested monadic types.Here’s what looks like:// Add this method inside MaybeMonad public MaybeMonad FlatMap(Func<int, MaybeMonad> func) { if (hasValue) { // Do not wrap again; let the callee decide whether to return a value or “nothing” return func(value); } // Propagate “no value” return this; } Use when your next step might also produce “no value,” and you want to keep chaining without ending up with .Maybe lookupUser(string id) { // Imagine this calls a database or external service and returns Maybe return GetUserFromDatabase(id); } Maybe userIdMaybe = GetUserId(); // Using Map would yield Maybe<Maybe> (nested) because lookupUser returns a Maybe. // This quickly becomes unwieldy and makes further processing difficult. var nested = userIdMaybe .Map(lookupUser); // Using flatMap collapses the result to a single Maybe var user = userIdMaybe .FlatMap(lookupUser); is arguably more important than , in fact, is required to qualify as a monad, and given you can implement in terms of it.What does this chaining look like procedurally? It would be similar to:string userId = GetUserId(); // could be null if (userId == null) { // e.g., return an error or stop here } User user = GetUserFromDatabase(userId); // this could return null (no user found) if (user == null) { // handle missing user } else { // we have a valid user } In the procedural version, we had to explicitly handle the control flow at each step (checking for in this case). In the monadic version, the control flow is implicit in the monad. If has no value, simply doesn’t call at all, the “else do nothing” logic is built into .In the monadic example, you could write:Maybe userIdMaybe = GetUserId(); Maybe userMaybe = userIdMaybe.FlatMap(lookupUser); The monads handle the control flow for us. returns a because we’re acknowledging the user ID might not exist. We’ve defined the monad such that if there’s no value, any subsequent function (like ) won’t execute. There’s nothing mystical here, we explicitly designed to work that way.This is why it makes sense to wrap values in monads and keep chaining within the monadic context: you can sequence operations (like getting a user ID, then looking up a user, then perhaps fetching their profile) without writing a single explicit or loop for the control flow. Each monad step handles the logic of “if there’s no value, stop here” automatically.If you prematurely yank a value out of a monad, you end up doing manual work that defeats this benefit. For instance, consider if we had a method to extract the inner value (with representing “no value”):Maybe userIdMaybe = GetUserId(); var actualUserId = userIdMaybe.GetValue(); if (actualUserId != null) { // do something with actualUserId } Eww. If we treat the monad as just a fancy wrapper to put a value in and then take it out immediately, it does feel like pointless ceremony. This is where many people give up on learning monads, it seems like you’re just putting a value in a box and taking it out again with extra steps. But the power of monads comes when you stay the monadic context and keep chaining operations. In Part 2, we’ll look at more advanced monads that aren’t just simple containers, and you’ll see how staying in the monadic pipeline pays off.Closing the loop on MaybeWe’re making a few changes to the monad to give it a more official, ergonomic API. First, instead of letting callers construct the underlying representation directly, we’ll expose two : and . Second, we’ll generalize map: instead of only mapping over integers, the monad will be generic so it can map any type. Finally, we’ll standardize the name to . Together, these tweaks clean things up and make the monad easier to use across more scenarios.public sealed class Maybe { private readonly bool _has; private readonly T _value; private Maybe(T value) { _has = true; _value = value; } private Maybe() { _has = false; _value = default(T); } public static Maybe Some(T value) { return new Maybe(value); } public static Maybe None() { return new Maybe(); } public Maybe Map(Func<T, U> f) { if (_has) { return Maybe.Some(f(_value)); } return Maybe.None(); } public Maybe Bind(Func<T, Maybe> f) // aka FlatMap { if (_has) { return f(_value); } return Maybe.None(); } } To wrap up : it’s perfect when you only need to model “value or no value.” Often, we also need to know a value is missing (not found, invalid input, business‑rule violation). can’t carry that reason.To be a true monad, a type must not only provide and operations, but also obey three simple laws that make sure these operations behave consistently: is the same as . (Wrapping a value and then immediately applying a function to it is equivalent to just calling the function on the raw value.) is the same as . (If you a monad with the function, the monad should remain unchanged.) is the same as m.flatMap(x => f(x).flatMap(g)). (It doesn’t matter how you parenthesize nested operations, the outcome will be the same.)You don’t need to memorize these laws, but they provide a mathematical guarantee that monadic operations will compose reliably. Our adheres to these laws, making it a true monad.As we’ve seen, monads provide a context for computation. By defining two core operations, (to wrap a value) and (to sequence operations that produce a new context), we abstract away manual control flow like loops and null-checks. This lets us turn scattered procedural code into a single declarative pipeline.The real power comes when we apply this pattern to different contexts. In Part 2, we’ll explore other useful monads, like for more descriptive error handling, and see how to combine monads to manage multiple concerns at once.Exercise for reader: I’d encourage opening up your IDE, without any AI assistance, and implementing the monad from scratch (no cheating.)