Intro
The term “monad” often arises in discussions about functional programming concepts and techniques. A monad is a design pattern used to structure code in a functional style while dealing with side effects, asynchronous operations, or computations that may fail.
In C#, the most common example of a monad is the Task<T>
type, which represents an asynchronous operation. The Task<T>
type allows you to work with asynchronous code in a composable and predictable manner, similar to how monads operate in functional programming languages like Haskell.
The result pattern often involves the use of the Result<T>
type or its variations. The Result<T>
type is commonly used to represent the result of an operation that can either succeed with a value of type T
or fail with an error. It encapsulates the result in a way that allows you to handle both success and failure cases in a unified manner, without resorting to exceptions.
Error Types
My approach to designing the results pattern comes with the following styles in mind:
- the functional side of the code should be an extension, not a rule
- the main paradigm should remain OOP and these structures should not be aware of the Result structure
- The
Result<>
should store both the value as well as the error. But the error can have various forms and we should be as explicit with the error as possible. The object should contain a generic Error class - Unhandled errors should be captured and returned in the
ErrorCapture
object - Our logic should be able to throw an exception which can be easily converted in a known error. This is performed by the
ErrorException
type
Here are the error classes I defined:
public abstract record Error; public sealed record ErrorCapture : Error { public required Exception Exception { get; init; } } public class ErrorException : Exception { public required Error Error { get; init; } } public sealed record ErrorChain : Error { public List<Error> Errors { get; } = []; }
Result Types
The abstract non-generic Result
type contains the base error class and various static helper methods for building the result
objects.
The ErrorChain(...)
method’s purpose:
- The end purpose of these sets of structures is to chain methods together and not worry about errors during the logical coding process. But each method we chain may return a different result.
- This method will convert
Result<TMethod1>
toResult<TMethod2>
which only works if values are not considered, only errors can be transferred without caring about the generic type
public abstract record Result { public Error? Error { get; init; } public static Result<T> Ok<T>(T value) => new Result<T> { Value = value }; public static Result<TOut> ErrorChain<TIn, TOut>(Result<TIn> result) { if (!result.IsError()) throw new Exception("THe provided result is not an error"); Error inError = result.Error!; ErrorChain outError = new ErrorChain(); if (inError is not ErrorChain inErrorChain) { outError.Errors.Add(inError); } else { outError.Errors.AddRange(inErrorChain.Errors); } return new Result<TOut> { Error = outError }; } public bool IsError() => this.Error is not null; }
The typed result class is the one that stores the actual value
public record Result<T> : Result { private readonly T? _value; public T Value { get { if (IsError()) { throw new Exception("Cannot get a value of an errored result"); } return _value ?? throw new Exception("The result not initialized"); } init => _value = value; } }
The execution logic is carried by the following set of methods which will:
- capture the exception if the function fails
- unwraps the exception if it contains a known error type
public static class MonadExecute { public static Result<TRet> Run<T, TRes, TRet>(this T input, Func<T, TRes> action, Func<T, TRes, TRet> returner) { try { TRes res = action(input); return Result.Ok(returner(input, res)); } catch (Exception e) { return new Result<TRet>() { Error = new ErrorCapture() { Exception = e } }; } } public static Result<TOut> Chain<TIn, TRes, TOut>(this Result<TIn> input, Func<TIn, TRes> action, Func<TIn, TRes, TOut> returner) { if (input.IsError()) return Result.ErrorChain<TIn, TOut>(input); try { TRes res = action(input.Value); return Result.Ok(returner(input.Value, res)); } catch (ErrorException ee) { return new Result<TOut>() { Error = ee.Error }; } catch (Exception e) { return new Result<TOut>() { Error = new ErrorCapture() { Exception = e } }; } } }
Business Logic Classes
Let’s imagine we have the following structures in our business logic (wouldn’t be nice if everything was so simple):
public record GameErrorDuplicatePlayer(string PlayerName) : Error; public class GameSettings { private readonly string _chatName; private GameMap? _map; private readonly List<GamePlayer> _players = new(); public GameSettings(string chatName) { _chatName = chatName; } public GamePlayer AddPlayer(string playerName) { if (_players.Any(x => x.Name == playerName)) { throw new ErrorException() { Error = new GameErrorDuplicatePlayer(playerName) }; } var player = new GamePlayer(playerName); _players.Add(player); return player; } public GamePlayer? GetPlayer(string playerName) { return _players .Find(x => x.Name == playerName); } public GameMap AddMap(string mapName) { if (this._map is not null) { throw new Exception("The map was already loaded"); } _map = new GameMap(mapName); return _map; } } public class GamePlayer { public string Name; public GamePlayer(string name) { Name = name; } } public class GameMap { public string Name; public GameMap(string name) { Name = name; } }
Usage: Explicit
One way of using the result pattern as an extension of our business logic is to define a service that would define the service methods and this service would return results instead of business domain types.
The advantages of using this method is that the monadic implementation is explicit and it’s the service mandates this structure.
The disadvantage is that you have to write more code which makes the implementation harder
public static class GameManager { public static Result<GameSettings> CreateGame(string chatName) { return Result.Ok(new GameSettings(chatName)); } public static Result<GameSettings> AddPlayer(this Result<GameSettings> game, string playerName) { return MonadExecute.Chain(game, g => { g.AddPlayer(playerName); return g; }); } public static Result<GameSettings> AddMap(this Result<GameSettings> game, string mapName) { return MonadExecute.Chain(game, g => { g.AddMap(mapName); return g; }); } }
Result<GameSettings> game = GameManager .CreateGame("combat") .AddPlayer("john") .AddPlayer("john") .AddPlayer("doe") .AddMap("europe");
Usage: Extensions
Unlike the previous method, here we are defining a set of extension methods that will convert any operator in a result.
This way has the advantage of making the code lighter and easier to implement.
The disadvantage is that the code is harder to read as every operation is wrapped in the Chain
method
public static class MonadExecute { public static Result<TRet> Run<T, TRes, TRet>(this T input, Func<T, TRes> action, Func<T, TRes, TRet> returner) { try { TRes res = action(input); return Result.Ok(returner(input, res)); } catch (Exception e) { return new Result<TRet>() { Error = new ErrorCapture() { Exception = e } }; } } public static Result<TOut> Chain<TIn, TRes, TOut>(this Result<TIn> input, Func<TIn, TRes> action, Func<TIn, TRes, TOut> returner) { if (input.IsError()) return Result.ErrorChain<TIn, TOut>(input); try { TRes res = action(input.Value); return Result.Ok(returner(input.Value, res)); } catch (ErrorException ee) { return new Result<TOut>() { Error = ee.Error }; } catch (Exception e) { return new Result<TOut>() { Error = new ErrorCapture() { Exception = e } }; } } } Result<GameSettings> game2 = new GameSettings("combat") .Run(g => g.AddPlayer("john"), (g, _) => g) .Chain(g => g.AddPlayer("john"), (g, _) => g) .Chain(g => g.AddMap("europe"), (g, _) => g);
Thank you for reading. If you liked this article you can help me by sharing it. If you want to read more similar articles consider subscribing to my newsletter.