Back to Blog List

The Ultimate And Final Monad Tutorial You’ll Ever Read

2024-10-21

Welcome to this tutorial. If you're looking to learn about Monads and their role in programming, then you've come to the right place. I'm hoping that this will be the final tutorial you'll ever need to read about Monads. What are Monads you ask? I hope that by the end of this article you will know what they are, how they are useful, and where they are useful, in the context of Software Engineering.
Why should you care? Perhaps you're a curious person by nature. Maybe, you've been in the software industry for a while and have stumbled across the concept while reading about functional programming.
Why invest your time in this tutorial as opposed to reading the other hundreds of tutorials out there? The answer is that almost all of the articles and tutorials that I've stumbled across during my multiyear journey of learning functional programming, share very similar problems. The first category of tutorials take the mathematical approach. Since Monads are a mathematical construct, from a field of Mathematics called Category theory, they try to teach you about them from that perspective. The problem is, that unless you are truly willing to invest in learning this very abstract field (some say very difficult to comprehend) you will find these tutorials very obscure and unintuitive. The second category of tutorials, take a different path. They describe to you what the problems that Monads solve and then give you a plethora of examples of how to use them. All of them follow a similar template and none of them truly help their readers build the intuition of what Monads are.

My promise to you, the reader, is that instead of spoon-feeding you with terminology, I will help you build the intuition from the grounds up. From first principles. This is the only way in my humble opinion to truly internalize concepts. Especially abstract concepts such as these. My only ask of you is to be patient. Don't jump to the end trying to read the conclusions. Instead focus on the journey. It is that path that will help you really understand the concepts and internalize them.

Why So Difficult?

In math and computer science there are certain abstractions and concepts that beginners always find hard to grasp. For example, recursion in computer science, or pointers when learning to program in C , and many more. Monads fall into this category as well. They are a very abstract mathematical concept introduced into the programming world in the nineties of the previous century. They were introduced into programming because their mathematical properties and semantics were a perfect fit for a particular set of problems that arose primarily in the context of functional programming. The good news is that by building the intuition of why they were introduced, you will be able to finally understand them. So Lets get started shall we?

Building The Motivation

You are a senior software engineer who has been writing code for a long time. You pride yourself in writing clean, modular code that rarely breaks. One day you are tasked to write a feature. The essence of the feature is not important, but being the great engineer you are, you write it in a very modular way. You use interfaces for contracts, you expose the right sets of APIs, and you use dependency injection to thread those interfaces properly through your code. At some point you realize that you need to create an HTTPRequest Object from a URL. Surely, someone else has already written such a helper method before you. So you look at the code, and sure enough you find the following API:

HttpRequest CreateHttpRequest(String Url);

It was written many years ago by an engineer who had left the company. The signature suggests that it is exactly what you need. But you decide to take a closer look into what it does. You see that it makes many validations, You notice that it makes many other calls, some of which are nested deep inside. But you feel confident that this is a great fit for your needs. Finally, you're done with your project. You ship it to production. You feel proud. But then…. an incident.. and another one… and another one. At 12 AM, you're called into a bridge to help with the escalations. The culprit has been identified. It is your new feature! How could it be? You've tested it so well before shipping. You start digging into the code and find out that the CreateHttpRequest method is throwing an exception. The author of that method (the one who left the company) expected the callers of it to handle the exception gracefully. But you didn't know. You didn't even see that this method was throwing an exception. You dig through the implementation and find that in one particular branch, there is a validation that you didn't know about. That validation is the one that throws the exception. It couldn't have been caught before, unless you knew about it and wrote a specific test for it. But when you reviewed the code, you missed it. What a shame!

Functional Programming and Pure Functions

The problem I described above either happened or could have happened to any of us at some point. It would have been nice of the method's developer to leave some comment in the code that warned you of this exception. Even better, if the developer would have annotated the function with the fact that it throws the exception. For example in Java or in C++ you can add the "throws" keyword to your method's signature followed by the exceptions that it throws. Even better than any of these, if the developer wouldn't have thrown the exception at all. But we'll get to that in a bit. In Functional Programming, specifically strongly typed languages, like ML, Haskell or Scalla, these problems don't happen. The reason is that in Functional Programming functions are "pure". Pure in this context means that the functions behave like mathematical functions. They are deterministic, predictable, and they don't produce any side effect. Lets take a look at the following math function:

$f(x) = x^2$

When you plug in it the number 2 you always expect it to return 4. No matter how many times you call it. It would always produce the same output for the same input. You'd be very surprised if I told you that this function in addition to returning 4, also prints "hello world" to the screen, or perhaps sends a TCP packet. In math this function is pure, and in FP the same holds. When you have pure functions in your code, life becomes much easier. Everything is deterministic and predictable so the code is less error prone. The code is much more readable, as you can understand the functions by looking at their signature. The code is easily maintainable, as refactoring it is relatively easy. You needn't be concerned with the side effects that occur and that you need to take care of during the refactoring. Most of all, the code supports composability.

Just like in math where you can compose functions, you can do the same with pure functions in your code. So long as the type of the output of one of the functions matches the type of the input of the other one, it is guaranteed to work correctly. In fact you could define a new function that is the composition of the other two, and you would know exactly what it produces. This would not be the case when one of these composed functions throws an exception for example. In that case, the composition breaks. In FP, function composition is the canonical way of creating abstractions. You break a problem into functions that solve parts of it and then compose them back together to get the final solution. In languages like Haskell, programmers half jokingly say that if your code compiles then your program works. Hopefully I've been able to convince you of the merits of writing pure functions that don't have any side effects. If you're still not convinced, well… good luck with those escalation bridges at 12:00 AM. But if you are convinced, great! we're done.

Dealing with (Side) Effects, The Functional Way

Well, we are not really done. A program that doesn't have any effects is useless. After all, we write programs to solve real world problems. If the program can't interact with the real world, then it has no purpose. Furthermore, effects are not evil. They are essential for our program to do its work. The problem with them is that if they are not done in a controlled and deterministic way, then our program suffers. All the benefits we mentioned above are lost. Ideally we want to handle effects in a manner that does not break the "purity" of our functions. Can this be done? The answer is yes!
To get an intuition of how we can do it, we need to take a step back. Think about a case where you need to call a database to query a table.

In most programming languages you will be using a library that interacts with the DB and abstracts away all the messy parts of connecting to the DB, passing in the connection string and so forth. That library exposes a set of interfaces that allow you to call the DB and run your queries. In essence that libraries' APIs are promises to do the right things and get you a result that you expect. When you write your code, you are implicitly taking a dependency on those promises, by the mere fact that compilation passes. But the truth is that only during run time you will find out if the library is doing the right thing. When you are writing your code, calling the DB library, you assume that it will return to you the information you need from the DB and based on that assumption you write the rest of your code. Layers upon layers of software are built on this idea. Each layer abstracts the layer beneath it, and uses the returned values from the layers beneath to perform calculations and to transform them into something else that their callers need. That is the essence of Software Engineering.

All of these are "hoping" that this library, at the bottom most layer is doing its job as expected and that during run time when its called, it will indeed return the values that the layers above are expecting.

A Promise Must Always Be Kept

What if instead of dealing with this "promise" implicitly, by hoping that in runtime things will work correctly, we turn it into an explicit promise. By being explicit I mean that we declare our intention of doing the effect by codifying the promise and letting the compiler enforce it. But how can we do that? What tools do we have at our disposal when we write our code before and during compilation time that we can use to encode a promise to perform some action? why, our type system of course. We need to create a new type that represents the promise to perform an action (read from the DB, write to a file, print to the screen, or throw an exception).

What are the requirements from this type? First, it needs to pass compilation. In other words we can't invent a new construct that is not part of our language. Second, we want it to represent the promise as intended. For example, if the promise is to call the DB, run a query and return a User object, then that type has to be declared in terms of a User object - the result of the query. If the promise is to handle errors gracefully, then that type needs to be declared in terms of an error or an exception type. Most languages today support this construct by means of parameterized types (aka Generics, Templates etc..).

The official term for a type that can be parameterized with other types is a Type Constructor. So our Promise type needs to be a Type Constructor, that is parameterized with the type of the result of the computation that it promises perform. For each (side) effect we can create a promise type representing the computation that has this effect, and parameterize the promise with the type of the result of that computation. However, we will soon find out that this is not enough. For our type to work correctly and as intended it needs to adhere to certain laws, and provide certain guarantees. I could spoil the fun and tell you what these laws are, but I much prefer to continue developing the intuition for them ourselves.

Example of a Promise

We are writing a small program that receives an ID of a user and finds that user's favorite song. This is a toy example, and so I will make it unnecessarily complicated, with the intension of using it for educational purposes.

Our code needs to read from the Users table a user corresponding to some ID and return all of that user's properties: Name, Last Name, Age, Gender, etc…
The layer above it, takes this User object that we queried and extracts their name and last Name and then concatenates them into a single string. The layer above that takes the concatenation and calls another DB where we store information about songs songs that a user uploaded. That layer gets back a the result from that query in a form of a Song object, which has properties like: ID, Song_Name, Date_Uploaded, Rank. It wants to return the favorite song of that user based on the Rank field.

First we recognize that our program has two methods that produce effects when they query the DB the first that queries the Users table, and the second that queries the Songs table. But the rest of the methods, in the upper layers don't do any effects. Instead they take the results given to them and transform them. For the methods that perform queries on the DB we will create a special type for a promise:

public class DBPromise<T>
{
    public T RunDBQuery(...){...}
}

Remember that T is the type of the result of performing that action of calling the DB. It is important to remember that the value corresponding to T will not be produced until we actually call the DB. How does DBPromise<T> produce the value corresponding to T? We will assume that it has a method called RunDBQuery(…) that takes the parameters needed to run the query and returns the value corresponding to T.

The three dots in the method's arguments list is to abstract away the details required to do the actual call. Notice that this is the method that actually calls the DB and hence it performs the effect. Hence RunDBQuery(…) isn't a pure function. We'll get back to it soon.
Lets look at all the important methods in our example to see what their signatures are before we modify them to work with our Promises.

// returns a user's information from the DB
User QueryUserById(long Id);

// returns a user's favorite song from the DB based on their first and last name 
Song QueryFavoriteSongByUserName(string NameAndLastName);

// calls QueryUserById and returns their first and last name concatenated 
string GetUsersNameFromId(long Id);

// calls QueryFavoriteSongByUserName and returns the users favorite song's name
string GetFavoriteSongForUser(string UserNameAndLastName);

// main method of our program
void PrintUserFavoriteSong(long Id) 
{
   string userNameAndLastName = GetUsersNameFromId(Id);
   string favoriteSong =  GetFavoriteSongForUser(userNameAndLastName);
   Console.WriteLine(favoriteSong);
}

We start first with QueryUserById and QueryFavoriteSongByUserName and make them return a DBPromise<T>.

// returns a promise to read the user's information from the DB
DBPromise<User> QueryUserById(long Id);

// returns a promise to read the user's favorite song from the DB based on their first and last name 
DBPromise<Song> QueryFavoriteSongByUserName(string NameAndLastName);

We need to convince ourselves that they are "pure" according to the definition above. To be pure, they shouldn't be performing any effects. Meaning that they should not be calling the DB themselves. Otherwise, whats the point of have this DBPromise type? Instead what they need to do is to construct a DBPromise<T>. They will do so such that DBPromise<T> will have all the context it needs, so that when the time is right, we can call its corresponding RunDBQuery(…) and actually perform the query. How do they do that? One idea is to store the the SQL query as a string inside the DBPromise object. QueryUserById and QueryFavoriteSongByUserName will simply create the queries and store them as strings inside the DBPromise.

Then, when the time is right and we call RunDbQuery() on the DBPromise object, it will use that string to query the DB and get the result. That can work. But aside from the fact that it is a pretty lousy way of querying a database, we will also see why this doesn't solve our problem. In fact all it does is to kick the can down the road. However for now, we have confidence that so long as QueryUserById and QueryFavoriteSongByUserName don't actually call the DB, they are pure. For every Id QueryUserById() will always produce the exact same value of type DBPromise<User>. For every NameAndLastName QueryFavoriteSongByUserName() will always produce the same DBPromise<Song>.

What about GetUsersNameFromId(long Id)? It needs to call QueryUserById(Id) to get the User object out of which it needs to extract the first name and the last name. But now, instead of getting the actual User object it gets back a promise. Our famous DBPromise<User>. In order to extract the names from the user It needs to extract the User from the promise. How can it do that? our first attempt might be to call RunDBQuery() and get back the User from the DBPromise. There is only one problem with that.

By doing so, GetUsersNameFromId(long Id) can no longer be a pure function. Hence my previous statement about kicking the can down the road. Lets go back to the drawing board. We can't just encode the query as a string in the DBPromise object. We need something else. What we need is a deferred function. In Java, C++ and C# we have lambda functions. In C# we also have delegates which were there even before lambdas were introduced into the language. We can also have deferred functions in C, by using function pointers as so:

// deferred DB Query method to get a User from the DB
typedef User (*UserQuery)();

// deferred DB Query to get a Song from the DB
typedef Song (*SongQuery)();

In all these languages the idea is the same. We want to encapsulate the effect in a deferred method inside the DBPromise<T> object. When QueryUserById(long Id) and QueryFavoriteSongByUserName(string NameAndLastName) are called, they construct their corresponding ***DBPromise<T>***objects by passing into them a deferred method (a Lambda, Delegate or function pointer). These will encapsulate the context required for the effect to take place. Then when the time is right, and we want to run the query for the User or Song, we call the DBPromise<T> object's RunDBQuery(…) which will in turn invoke our deferred method.

At this point you might be asking yourself how does this help? Why is this not considered kicking the can down the road? After all, we will still need to extract the User or the Song from the promise to do the transformation. Do we? Can we somehow still operate on the User or Song in a way that doesn't require us to actually call the DB? Remember, its all just promises.

Functors to the Rescue

Lets get back to our GetUsersNameFromId(long Id). Remember that it called QueryUserById(long Id) and received back the DBPromise<User> and now it needs to operate on the User to get the name and the last name. We also said that we don't want to make the call to the DB here so to not break the purity of this method. Here is what we really want to do:

// we want to turn this method:
string GetUsersNameFromId(long Id);

//into this:
DBPromise<string> GetUserNameFromId(long Id){
    DBPromise<User> userPromise = QueryUserById(Id);
     
    // if we had a User instead of a DBPromise<User> we would do:
    // return User.Name + User.LastName
    
    return ????
}

What we really want to do is to elevate this method from a method that has a User and returns a string to a method that has a DBPromise<User> and returns a DBPromise<string>. Why? because so long as we return promises, which are basically deferred effects, we maintain the purity of our methods. The question is how to do this elevation. Luckily for us Mathematicians and Computer Scientists found a great way to do this sort of elevations. In Category theory, that branch of math which I mentioned at the beginning, it is called a Functor.

Functional Programming languages use the same name to describe the exact same construct in programming. At this point you must be very frustrated. I promised you that this is a tutorial about Monads. Yet so far all you've been getting are false promises - pun intended. Now, I'm talking about something called a Functor. Where the heck is the Monad in all this? To alleviate some of your frustration, I will mention that we have been talking about Monads all along. Yet, I have been purposefully avoiding calling it that directly. But now that we are starting to talk about Functors we might as well call it out. Our DBPromise<T> will turn into a Monad over the course of this tutorial. But it is first and foremost a Functor. Confused? stay tuned.

A Functor in programming languages is a Type Constructor. Meaning it is a Type that is parameterized with another generic type. But it also has a method that it must support. This method is sort of like an interface of the Functor if you will. In different languages this method is called different things. In Haskell its called fmap. In Lisp its called map. In Java's streams it is called map. In C++ it is called std::transform(…). In C# it is part of the LINQ library and its called Select(…). While the names are different, the essence stays the same. In all cases these methods are higher level functions - functions that take as an argument another function, and elevate them to a new type. Lets see a C# example for our DBPromise<User>:

public static DBPromise<string> GetUserNameFromId(long Id){
    DBPromise<User> userPromise = QueryUserById(Id);
    return userPromise.Select(user => user.Name + user.LastName);
}

All we did was to take our promise and "map" over its promised User value and return a new promise of type DBPromise<string>. The way to think about it is as follows: when the time is right an I call the DB to get a user, I promise that I will turn that user into a string concatenation of its first name and last name. Another way to think about it is that we are stacking up promises. We are taking a promise, and making up another promise based on our belief that the original promise will be kept.

The final question to be answered is, how does the Select method know how to operate on the User object? after all it receives a lambda that expects to get a user so that it can return the first name and last name and encapsulate them back into a DBPromise<string>. The answer is that DBPromise<T> needs to implement Select to work correctly. It does this by relying on the fact that it holds a deferred method inside it.

All it needs to do is to create a new DBPromise<String> which will hold a new deferred method. The new deferred method will first call the original deferred method, which will in turn do the DB operation and return a User. Then the new deferred method will apply the lambda inside the select on the User object to get the string. Notice that by doing all this inside a new deferred method, we are still keeping everything pure. So long as it is all happening within the confines of a deferred method that doesn't get executed, we are good. Lets see the code:

public class DBPromise<T>
{
    // The lambda or function that queries the database and returns a value of type T
    private readonly Func<T> query;

    // Constructor that takes a function (lambda) to perform the database query
    public DBPromise(Func<T> query)
    {
        this.query = query;
    }

    // The Select method (similar to 'map' in functional programming)
    public DBPromise<U> Select<U>(Func<T, U> transform)
    {
        // Return a new DBPromise<U> where the query result is transformed using the 'transform' function
        return new DBPromise<U>(() =>
        {
            // Execute the original query and apply the transformation function 'transform' to its result
            T result = query();  // This runs the original query
            return transform(result);  // Transform the result using the provided function
        });
    }

    // Method to execute the query and retrieve the result
    public T RunDBQuery(...)
    {
        return query();
    }
}

Lets recap. To make the chain of calls work correctly, we need to pass up the call stack a DBPromise<T>. To do that properly, our DBPromise<T> needs to be a Functor. To be a Functor it needs to be first a Type Constructor, which it is. Second it needs to implement the map/fmap/Select method. You can think of Functor as an interface with a single method and DBPromise<T> as a class which implements that interface. By implementing the interface method, we are able to continue passing up the call stack DBPromise<T> objects, each representing not only the original promise but also promises to do transformations on the result. Don't worry, at some point we will need to call the database and perform the actual event. But we want to wait with it to the last possible moment. We will talk about why that is towards the end.

For DBPromise<T> to be a Functor it is not enough to just implement Select(..). It also needs to obey two important rules. Those rules are known as the Functor laws. The first law is called the identity law and it can be demonstrated in code as follows:

// Self just returns back the input that it received without modifying it
User Self(User user){
  return user;
}

DBPromise<User> selfPromise = DBPromise<User>.Select(u => Self(u));

The Identity law says that just like Self(User user) leaves the user unaltered, so that running Select with it on an existing DBPromise<User> doesn't alter that promise. In other words, this tells us that the Functor should not do any modifications to the underlying object on top of what the lambda that is passed to Select does. All the Functor is allowed to do is to call that lambda and use the result of it to encapsulate it inside a new Functor. The second rule is called the rule of composition and we can show it in the following code:

User CreateDummyUser(Long Id){
  return new User(...);
}

string GetNameFromUser(User user){
  return user.name;
}

string CreateDummyUserAndGetName(Long Id){
    return GetNameFromUser(CreateDummyUser(Id));
}

DBPromise<User> p1 = DBPromise<Long>.Select(id => CreateDummyUser(id));
DBPromise<string> p2 = p1.Select(user => GetNameFromUser(user));
DBPromise<string> p3 = DBPromise<Long>.Select(id => CreateDummyUserAndGetName(id));

The composition rule says that p2 and p3 in the code above should be equivalent to each other. In other words, p2 is created by taking p1 and calling select on it. You can think of it as us composing the two Functors p1 and p2. Then, p3 is created by us first composing the two functions together and then applying the composition within the Select. This rule ensures that if to functions f1 and f2 are composable, then the Functors created by them (using Select) are also composable.

The two laws of the Functors are not arbitrary. They come directly from the definition of Functors in Category theory. In the context of Programming however, they are important because they give us certain guarantees about our code. Specifically about our Promise Types. Functors that respect the identity law guarantee that they won't modify the underlying structure, and when they respect the composition law, we are guaranteed that we can chain them together and not worry that the chaining will somehow break the underlying structure and the create unpredictability.

I want you to stop for a second and see the continues motif that shows up over and over again. We are constantly trying to ensure that our functions, which in FP are the building blocks of our programs, are composable, are pure and deterministic. To that end, even when we operate on them by elevating them to new constructs such as a Functor, we continue to insist on those characteristics. By insisting on these things, we reassure ourselves that the program we are writing is correct and doesn't break its invariants.

The Good, The Bad, and the Ugly

First of all congratulations for reaching this far. As I said in the beginning, its a journey, and the journey is what teaches us. What have we learnt thus far:

  • We want our functions to be mathematically pure. Pure is good as it saves us from bugs, it makes our code more readable and maintainable, and it allows us to rationalize about it easily.

  • Programs have effects. They interact with the outside world, they change state, they handle errors, they spawn threads and all these effects are inherently contradicting to the pureness that we strive for.

  • We can't eliminate the effects, but we can somehow make them fit into our pure world in a way that they are controlled.

  • We do so by creating promises. These promises are types that represent a deferred computation of an effect, and encapsulates the result of that effect.

  • By having our functions return these promises rather than doing the effects themselves, we are turning them into pure functions.

  • In order to make sure that the callers of these functions don't need to execute themselves the effects and hence break their own purity, we need to make sure that these promises allow us to transform and compute on their encapsulated result without actually having to execute the effects. We use an FP construct called a Functor to do that. Luckily for us, most languages support Functors and most of you have used them in your day to day programming without even knowing that this is what they are called.

  • The Functors, have two laws - Identity and Composition. Those laws give us guarantees about their behaviors, and specifically that they don't break or modify the underlying structure on which they operate.

You might be worried that you'll never get to learn about what Monads are, at this rate. But rest assure that we are almost there. Monads are Functors with additional behaviors which we will get to, soon. I said it before, but lets rehash it in a more object oriented lingo. You can think of a Functor as an Interface with an abstract method Select (or map or fmap).

Any Type that wants to implement that interface needs to be first and foremost a parametrized type (aka. Type Constructor) and second of all must ensure that when it implements the Select/map API it obeys the two Functor laws. A Monad is also an interface with two APIs that we'll discuss. But Monad itself implements also the Functor interface, and augments the Functor to be even more powerful. In many ways, we can solve the majority of our problems related to effects in a pure world, by simply sticking to the Functor realms. As we showed by passing the Promise results up the call stack, and allowing the upper layers to augment it we maintain the purity of the entire call chain.

The downside of course, is that now every chain of calls that at some point performs an effect, needs to be aware of that effect all the way to the top. Another downside is that once we have committed to a promise type, we can't modify it to a new promise type. For example, if we encapsulate DB calls within a DBPromise<T>, we can't transform it to a HttpRequestPromise<T>, that is unless you're willing to break the purity and cause the effect. Our Functor interface only allows us to move from DBPromise<T> to DBPromise<U> or from HttpRequestPromise<X> to HttpRequestPromise<Y>, but not between them. Don't worry, Functional Programmers have found a way to work around that. But its beyond the scope of this tutorial, so we'll ignore that problem for now.

Another thing to notice is that we have an explicit expectation that every effect returns a value - the parameter T in the DBPromise<T>. What if an effect doesn't do anything? In other words, what if the effect returns void? Languages like Haskell solved it by having a built in Unit type. Its a type that can only have one value and is analogous to void. But it solves the problem of effects that don't return anything. You can create your own Unit type in the programming language of your choice.

When Should we Keep our Promises?

At some point in the call stack we will need to finally execute the effect. When is the right time to do that? The answer is like with everything else in life: "It depends". The recommendation is to keep the lower levels of your stack as "pure" as you can, and let the upper most layer, for example the layer that handles user requests, or the layer that has the main() method defined, deal with the effect. But it is really up to you the developer to decide the right balance between pure functions and non-pure functions and when it makes sense from a software engineering perspective to break the purity. The point is that there is no silver bullet.

In Haskell, the language designers solved that problem for the developers. Haskell like any other programming language has a main() method. Except that in Haskell the main method must return an IO type which is a promise that encapsulates results of performing IO operations. Every Haskell program is required to return an IO promise at the end, and it is the runtime that eventually runs the effect of the promise.

When Functors Don't Cut It Anymore

Functors have brought us a long way. But they are very limited. To see their limitations lets continue with our example from before. Remember that we had two promises of type DBPromise<T>. One for Users which were transformed through the power of the Functor into a DBPromise<string>, and another one for Song. But at some point we need to do something like this:

// main method of our program
void PrintUserFavoriteSong(long Id) 
{
   DBPromise<string> userNameAndLastName = GetUsersNameFromId(Id);
   DBPromise<string> favoriteSongName = 
      GetFavoriteSongForUser(/* What should we pass here? */)
  
   ....
  
  // Print the name of the favorite song
}

GetFavoriteSongForUser(…) originally expected to get a string representing the user's name and last name. But now it has a DBPromise<string>. Moreover, it needs to use that string to call itself the DB and return back a DBPromise<string> representing the result of querying the Songs table and getting back the favorite song. Since we have a hammer called Functor, lets try to use it on this nail:

DBPromise<string> userNameAndLastName = GetUsersNameFromId(Id);
<????> result = userNameAndLastName.Select(name => QueryFavoriteSongByUserName(name));

Quick quiz to check your understanding. What is the Type that corresponds to ???? above?
a. DBPromise<string>
b. DBPromise<DBPromise<string>>
c. Compilation Error

The right answer is (b). Remember what a Functor does. It takes the result of the function passed to it and elevates it up to the Functor's Type. The lambda we passed to Select above returns back itself a DBPromise<string>, since it needs to query the Song's table. However the Select wraps it into another DBPromise<T> which turns the end result into DBPromise<DBPromise<string>>. This is a real pickle.

Monads To the Rescue

What we need is a way to unwrap the internal DBPromise<string> from the external one, all without actually causing the effect. We finally got here. I hope you had a great ride so far. This is where we can finally start talking about Monads. Just like Functors are a construct from Category theory so are Monads. As I said before, Monads are Functors, but they enhance the Functor interface and add two new APIs. Furthermore, in addition to the two Functor laws that they inherit from the Functor interface, they also come with a set of three laws of their own. For the curious minds out there, I will just say that in category theory the new Monad APIs that I'm referring to are called natural transformations. What are these two new APIs:

  • The first API referred to us return or pure, has the following signature:
// The 'return' (or 'pure') function: it simply wraps a value in the DBPromise context
    public static DBPromise<T> Return(T value)
    {
        // Return a new DBPromise that immediately returns the value without querying a database
        return new DBPromise<T>(() => value);
    }

all it does is to take a T and wrap it inside our Promise which we are turning into a Monad. This allows us to treat pure values as if they were results of database queries, even though no actual querying happens. You might ask yourself why we need it. The immediate answer is that the Return function allows for composability in monadic operations. For example, if we want to compose to DBPromises together as we will see soon.

  • The second API that the Monad needs to support is the unwrapping API. This is the API that will allow us to take a DBPromise<DBPromise<string>> and turn it into a single unwrapped DBPromise<string>. If you've programmed in Haskell you know this API by the name bind. If you've been programming in Java using Java Streams you may know this API by the name flatMap. In C# it is again part of the LINQ library and is called: SelectMany. Lets see the code for it as it pertains to DBPromise<T>:
// Monadic SelectMany (bind/flatMap) API
public DBPromise<U> SelectMany<U>(Func<T, DBPromise<U>> binder)
{
    // Return a new DBPromise<U> that, when executed, runs the original query,
    // then applies the binder function and runs the next query.
    return new DBPromise<U>(() =>
    {
        // Run the original query to get the result
        T result = this.Run();
        // Apply the binder function to the result, which returns a new DBPromise<U>
        DBPromise<U> nextPromise = binder(result);
        // Run the next promise to get the final result
        return nextPromise.Run();
    });
}

SelectMany takes in another function, which make it, just like its sister API Select, a higher order function. We call the function that it receives the binder because it binds the existing Promise to a new one. Lets get back to our example code to see how it will use SelectMany:

// main method of our program
void PrintUserFavoriteSong(long Id) 
{
   DBPromise<string> userNameAndLastName = GetUsersNameFromId(Id);
    
   DBPromise<string> favoriteSong = 
      userNameAndLastName.SelectMany(
          userName => GetFavoriteSongForUser(userName));

   // finally lets cause the effect 
   // we can do it here because we know that this is the upper most layer
   Console.WriteLine(favoriteSong.RunDbQuery());
}

Examples Of Monads And How They Can Be Used

At this point you should feel more confident in what Monads are. Now I can take the route that many other tutorials take which is to give you some additional Monad examples and what they are used for.

1. Maybe/Optional Monad (Handling Absence of Values)

  • Effect Managed: Optional values (handling null or missing values).
  • Description: The Maybe monad (called Optional in Java) is used to represent computations that might return a value or nothing. It encapsulates the idea of a value being present (Just or Some) or absent (Nothing or None). This is useful for avoiding null reference errors and handling missing data in a safe, composable manner.

2. Either Monad (Handling Errors or Branching Logic)

  • Effect Managed: Errors or Branching logic.
  • Description: The Either monad is used to represent computations that can result in one of two possible outcomes: a left value or a right value. It is often used to handle errors or to represent computations that can fail in a safe, composable manner. The Either type is typically used in combination with pattern matching or similar constructs to handle the two possible outcomes.

3. IO Monad (Handling Input/Output and Side Effects)

  • Effect Managed: Input/Output and Side effects.
  • Description: The IO monad is used to represent computations that involve side effects like reading from a file, writing to the console, or interacting with the outside world. In purely functional programming, side effects are deferred until the monadic value is explicitly executed. This allows programs to remain pure while modeling impure operations.

4. Future Monad (Handling Asynchronous Computations)

  • Effect Managed: Asynchronous computations.
  • Description: The Future monad is used to manage asynchronous computations that may complete at some point in the future. It allows chaining computations that depend on values that are not yet available, handling the asynchronous flow of data. This is common in modern programming languages for managing tasks like network requests or background operations.

5. List Monad (Handling Collections)

  • Effect Managed: Non-determinism or computations with multiple results.
  • Description: The List monad represents computations that may return multiple possible results. It allows you to chain computations that produce lists, flattening the results to combine all possible outcomes. This is often used in scenarios where a computation can result in several possible values, such as generating combinations or paths in a search algorithm.

Final Words

So here we are. We started with the fundamentals of pure functions, how they save us from bugs, make code easier to maintain, and how Functional Programming (FP) enforces purity. But we don't live in a pure world - our programs interact with the messy reality of side effects. Enter Functors - they helped us a lot by mapping over computations without breaking purity. Yet, we quickly saw their limits when we needed to chain computations that depend on one another.

This is where Monads come into play. They pick up where Functors fall short, giving us the tools to not only chain dependent actions but to do so while maintaining the guarantees of purity and composability. flatmap or SelectMany lets us unwrap nested computations, turning what would be a messy DBPromise<DBPromise<string>> into something clean and usable: a single DBPromise<string>.

Monads give us an elegant solution to managing effects - be it I/O operations, error handling, or asynchronous tasks - while still allowing us to compose our functions cleanly. If you're still scratching your head wondering why Monads are so celebrated, here's the truth: they're not some esoteric magic - they're just a great solution to the kinds of everyday problems you already face in your code.

Share this post