Lesson 4
Understanding Monads
Lesson Introduction

Monads are a powerful concept in functional programming that helps manage side effects and represent computations as chains of steps. When you encounter situations where functions may fail or return optional values, Monads provide a way to handle these cases elegantly. In this lesson, our goal is to understand what Monads are, how they work, and how to implement and use them in C++ to manage std::optional values safely and predictably.

Initial Example and Problem Contextualization

Consider a situation where you have a mock user database, and your task is to retrieve user information and then transform it into HTML format. Handling the absence of users or the failure of any step is crucial. Here's the example code that sets up our data and declares functions we want to implement.

C++
1#include <iostream> 2#include <optional> 3#include <string> 4#include <unordered_map> 5 6// Mock database 7std::unordered_map<std::string, std::string> userDB = { 8 {"user1", "John Doe"}, 9 {"user2", "Jane Smith"} 10}; 11 12// Function to get user's full name 13std::optional<std::string> user_full_name(const std::string& login); 14 15// Function to convert text to HTML 16std::optional<std::string> to_html(const std::string& text); 17 18int main() { 19 20}

Normally, we would approach this task with a functor from the previous lesson to handle optional values, but there is a problem: a functor wraps its answer into std::optional. If the answer is already an std::optional, we will get a nested optional structure, which is highly difficult to deal with.

In this lesson, we will implement a new version of our Functor – a Monad – which solves this problem.

Explanation of Monad Concept

A Monad is an abstraction that allows structuring programs generically. In our context, a Monad provides a way to sequence operations while handling potential failures gracefully. A Monad extends the functor's structure by adding a new method, called join.

Let's create a Monad:

C++
1template<typename T> 2class Monad { 3public: 4 template<typename Func> 5 auto transform(const std::optional<T>& value, Func func) -> std::optional<decltype(func(*value))> { 6 if (value) { 7 return func(*value); 8 } else { 9 return std::nullopt; 10 } 11 } 12 13 std::optional<T> join(const std::optional<std::optional<T>>& nestedValue) { 14 if (nestedValue) { 15 return *nestedValue; 16 } else { 17 return std::nullopt; 18 } 19 } 20};

It has a transform method, implemented in the same manner as in the Functor from the previous lesson. But we can't just use this method with our functions user_full_name and to_html, as they will return optional values, which will be additionally wrapped in optional by transform.

To address this, we provide a new method, called join. It is very simple – it just unwraps the optional, returning its value.

Functions

Now, let's implement function we are going to use.

The user_full_name function retrieves the full name of a user based on their login:

C++
1std::optional<std::string> user_full_name(const std::string& login) { 2 auto it = userDB.find(login); 3 if (it != userDB.end()) { 4 return it->second; 5 } else { 6 return std::nullopt; 7 } 8}

The to_html function converts the text to HTML format:

C++
1std::optional<std::string> to_html(const std::string& text) { 2 return "<h1>" + text + "</h1>"; 3}

These functions are quite simple. Note that they are defined to return an optional to make the program more flexable and safe.

Instantiation and Monad Usage

To use the Monad, create an instance and utilize join(transform()) to chain operations:

C++
1int main() { 2 Monad<std::string> monad; 3 std::optional<std::string> current_login = "user1"; 4 5 std::optional<std::string> result = monad.join( 6 monad.transform( 7 monad.join( 8 monad.transform(current_login, user_full_name) 9 ), to_html) 10 ); 11 12 if (result) { 13 std::cout << "HTML result: " << *result << std::endl; // HTML result: <h1>John Doe</h1> 14 } else { 15 std::cout << "No result found." << std::endl; // No result found. 16 } 17 18 return 0; 19}

You first retrieve the user's full name and then convert it to HTML, handling possible failures at each step.

Lesson Summary

In this lesson, we explored Monads and their significance in functional programming. We implemented a simple Monad in C++ and used it with std::optional to chain operations safely. Monads help manage computations involving optional values, leading to cleaner and more predictable code.

Now, let's move on to hands-on practice tasks to apply the concepts learned. You'll implement Monads to handle various operations and practice chaining functions with optional values. This practice is crucial for solidifying your understanding of Monads in real-world scenarios.

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.