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.
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.
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.
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.
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.
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.