Why I hated Monad, and why I start to love it

Exploring an alternative approach to flow and error management

Nash
4 min readNov 16, 2023

Some time ago, I wrote about Exception Handling, where I briefly mentioned monads, particularly the Result or Option types, used for handling errors as opposed to exceptions. Initially, I found myself averse to monads, despite encountering them in professional projects. My primary concern was their tendency to clutter the code.

Take, for instance, this typical command handler pattern:


public class SomeCommandHandler : QueryHandler<SomeCommandInput, SomeCommandResult>
{
private readonly ISomeRepository _someRepository;

public GetPrivateIdQueryHandler(ISomeRepository someRepository)
{
_someRepository= someRepository;
}

public async Task<Result<SomeCommandResult>> Handle(
SomeCommandInput command,
CancellationToken cancellationToken)
{
var entityResult = await _someRepository.GetEntity(command.Id);
if (!entityResult.IsSuccess)
return Result.Fail(entityResult.Error);

var someThingResult = entityResult.Value.DoSomething();
if (!someThingResult.IsSuccess)
return Result.Fail(someThingResult.Error);

var saveResult = await _someRepository.Save(someThingResult.Value);
if (!saveResult.IsSuccess)
return Result.Fail(saveResult.Error);
return Result.Success(new SomeCommandResult(saveResult.Value.Id));
}
}

This pattern necessitates continuous checks and wrapping of results/fails, making it cumbersome. It also leads to dependency and proliferation of similar lines in the calling code, much like issues arising with nullable types.

Misconceptions About Monads

A commonly encountered monad is the Result object:

public class Result<T>
{
public T Value { get; }
public bool IsSuccess { get; }
public string Error { get; }

private Result(T value)
{
Value = value;
IsSuccess = true;
}

private Result(string error)
{
Error = error;
IsSuccess = false;
}

public static Result<T> Success<T>(T value)
{
return new Result<T>(value);
}

public static Success(string error)
{
return new Result<T>(error);
}
}

The issue here is not the encapsulation of the result, but the public accessibility of the state. This exposure encourages direct use, leading to code pollution, similar to the nullable pattern.

Redefining Monads

A more effective monad should act as a black box, obscuring its state and validity checks from the user. This necessitates methods for safe status access and transformation:

public class Result<T>
{
private T _value { get; }
private bool _isSuccess { get; }
private string _error { get; }

private Result(T value)
{
_value = value;
_isSuccess = true;
}

private Result(string error)
{
_error = error;
_isSuccess = false;
}

public static Result<T> Success<T>(T value)
{
return new Result<T>(value);
}

public static Fail(string error)
{
return new Result<T>(error);
}

public TOut Match<TOut>(Func<T, TOut> success, Func<string, TOut> error)
{
if(_isSuccess)
return success(_value);
return error(_error);
}
}

This approach ensures safe state handling without manual intervention. However, simply replacing IsSuccess checks with Match calls isn't sufficient. The key is to maintain the black box for as long as possible, introducing methods like Map to transform the state within this encapsulated context.


public Result<TOut> Map<TOut>(Func<T, Result<TOut>> mapper)
{
if(_isSuccess)
return mapper(_value);
return Fail<TOut>(_error);
}

This method allows us to manipulate the state without direct exposure, eliminating the need for repeated success checks. Our refactored command handler now looks like this:

public class SomeCommandHandler : QueryHandler<SomeCommandInput, SomeCommandResult>
{
private readonly ISomeRepository _someRepository;

public GetPrivateIdQueryHandler(ISomeRepository someRepository)
{
_someRepository= someRepository;
}

public async Task<Result<SomeCommandResult>> Handle(
SomeCommandInput command,
CancellationToken cancellationToken)
{
var entityResult = await _someRepository.GetEntity(command.Id);
return entityResult.Map(DoSomething)
.Map(await Save)
.Map(ToCommandResult);
}

private Result<Something> DoSomething(Entity entity) => entity.DoSomething();

private Task<Result<SaveOperation>> Save(Something something) => _someRepository.Save(something);

private Result<SomeCommandResult> ToCommandResult(SaveOperation saveOperation) => new SomeCommandResult(saveOperation.Id);
}

// And then match on the command result:

var response = await someCommand.Handle(someCommandInput);
response.Match(s => Console.WriteLine(s.ToString()), e => Console.WriteLine($"An error happened: {e}"));

With this approach, you no longer need to worry about null values or invalid states; the Result monad handles these internally. This concept extends beyond just operation results to other types of monads like Option, Try, Either, etc. Each serves a specific purpose, but all adhere to the principles of state encapsulation and seamless monad transitions, as demonstrated by the Map method.

Future Considerations: Monad Integration in Object-Oriented Programming

The examples provided in this article are simplified to illustrate the fundamental concepts and benefits of using monads and chaining. While these examples serve their purpose, the real power and versatility of monads become evident when we consider their integration into object-oriented programming (OOP). Imagine transforming our traditional class models into monadic forms, where classes themselves embody monadic properties. This transformation allows classes to not only encapsulate their state and behavior but also to seamlessly participate in monadic operations.

By embedding methods like Map, Bind, and Do into our classes, we can create objects that are inherently capable of monadic transformations. This approach leads to a more fluent and expressive style of coding, where operations can be chained in a manner that maintains encapsulation and reduces side effects. Such an integration would encourage a shift in how we think about class design, leading to systems that are more robust and easier to reason about.

Furthermore, this paradigm shift offers intriguing possibilities for enhancing code reusability and readability. It could lead to the creation of a new class of design patterns and best practices centered around monadic principles in OOP. The potential for extending these concepts to areas such as error handling, asynchronous programming, and state management is immense, opening doors to more advanced and efficient software design strategies.

In conclusion, while the concept of monads is often viewed through the lens of functional programming, their integration into OOP can significantly enrich our programming models, offering a harmonious blend of functional and object-oriented paradigms. This fusion promises a future where the clarity and safety of functional programming coexist with the structure and familiarity of OOP, leading to more maintainable, scalable, and reliable software.

--

--