Monad II
Implementations, visuals, and pipelines
__ __ ___ _ _ _ ____ ____ ___ ___
| \/ |/ _ \| \ | | / \ | _ \/ ___| |_ _|_ _|
| |\/| | | | | \| | / _ \ | | | \___ \ | | | |
| | | | |_| | |\ |/ ___ \| |_| |___) | | | | |
|_| |_|\___/|_| \_/_/ \_\____/|____/ |___|___|
The first part built the abstraction: sequencing contextual computations with pure, bind, join, and the monad laws. It is time for implementations and examples.
Monad implementations #
We have seen how to define and implement monads in different languages. But there are more ways to do that also revealing some interesting insights.
Complete Monad Implementations #
Let's see how to implement monads from scratch, showing the three essential components: the type constructor, unit/return, and bind operations.
Haskell has first-class support for monads with the Monad typeclass[1]:
{-# LANGUAGE RebindableSyntax #-}
module Main where
import Prelude hiding (Either(..), Maybe(..), Monad(..))
-- The Monad typeclass definition
class Applicative m => Monad m where
return :: a -> m a -- Unit operation (lift value into monad)
(>>=) :: m a -> (a -> m b) -> m b -- Bind operation (chain computations)
-- Default implementation of >> (sequence operator)
(>>) :: m a -> m b -> m b
m1 >> m2 = m1 >>= \_ -> m2
-- Maybe Monad Implementation
data Maybe a = Nothing | Just a deriving (Show, Eq)
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just x) = Just (f x)
instance Applicative Maybe where
pure = Just
Nothing <*> _ = Nothing
(Just f) <*> something = fmap f something
instance Monad Maybe where
return = Just
Nothing >>= _ = Nothing
(Just x) >>= f = f x
-- Either Monad Implementation
data Either a b = Left a | Right b deriving (Show, Eq)
instance Functor (Either a) where
fmap _ (Left x) = Left x
fmap f (Right y) = Right (f y)
instance Applicative (Either a) where
pure = Right
Left e <*> _ = Left e
Right f <*> r = fmap f r
instance Monad (Either a) where
return = Right
Left e >>= _ = Left e
Right v >>= f = f v
-- List Monad Implementation
instance Monad [] where
return x = [x]
xs >>= f = concatMap f xs -- Apply f to each element and flatten
-- IO Monad (built into Haskell runtime)
-- instance Monad IO where
-- return = primitive operation
-- (>>=) = primitive operation
-- Example usage
safeDiv :: Double -> Double -> Maybe Double
safeDiv _ 0 = Nothing
safeDiv x y = Just (x / y)
calculation :: Maybe Double
calculation = do
x <- Just 10
y <- Just 2
z <- safeDiv x y
return (z * 3)
-- Result: Just 15.0
-- Using bind directly
calculation' :: Maybe Double
calculation' = Just 10 >>= \x ->
Just 2 >>= \y ->
safeDiv x y >>= \z ->
return (z * 3)
main :: IO ()
main = print (calculation, calculation')
TypeScript doesn't have built-in monads, but we can implement them using classes and interfaces:
// Generic Monad interface
interface Monad<T> {
bind<U>(f: (value: T) => Monad<U>): Monad<U>;
map<U>(f: (value: T) => U): Monad<U>;
}
// Maybe Monad Implementation
abstract class Maybe<T> implements Monad<T> {
abstract bind<U>(f: (value: T) => Maybe<U>): Maybe<U>;
abstract map<U>(f: (value: T) => U): Maybe<U>;
abstract isSome(): boolean;
abstract isNone(): boolean;
static pure<T>(value: T): Maybe<T> {
return new Some(value);
}
static none<T>(): Maybe<T> {
return new None<T>();
}
}
class Some<T> extends Maybe<T> {
constructor(private value: T) {
super();
}
bind<U>(f: (value: T) => Maybe<U>): Maybe<U> {
return f(this.value);
}
map<U>(f: (value: T) => U): Maybe<U> {
return Maybe.pure(f(this.value));
}
isSome(): boolean { return true; }
isNone(): boolean { return false; }
getValue(): T { return this.value; }
toString(): string { return `Some(${this.value})`; }
}
class None<T> extends Maybe<T> {
bind<U>(f: (value: T) => Maybe<U>): Maybe<U> {
return new None<U>();
}
map<U>(f: (value: T) => U): Maybe<U> {
return new None<U>();
}
isSome(): boolean { return false; }
isNone(): boolean { return true; }
toString(): string { return "None"; }
}
// Either Monad Implementation
abstract class Either<L, R> implements Monad<R> {
abstract bind<U>(f: (value: R) => Either<L, U>): Either<L, U>;
abstract map<U>(f: (value: R) => U): Either<L, U>;
abstract isLeft(): boolean;
abstract isRight(): boolean;
static left<L, R>(value: L): Either<L, R> {
return new Left<L, R>(value);
}
static right<L, R>(value: R): Either<L, R> {
return new Right<L, R>(value);
}
}
class Left<L, R> extends Either<L, R> {
constructor(private value: L) {
super();
}
bind<U>(f: (value: R) => Either<L, U>): Either<L, U> {
return new Left<L, U>(this.value);
}
map<U>(f: (value: R) => U): Either<L, U> {
return new Left<L, U>(this.value);
}
isLeft(): boolean { return true; }
isRight(): boolean { return false; }
getLeft(): L { return this.value; }
toString(): string { return `Left(${this.value})`; }
}
class Right<L, R> extends Either<L, R> {
constructor(private value: R) {
super();
}
bind<U>(f: (value: R) => Either<L, U>): Either<L, U> {
return f(this.value);
}
map<U>(f: (value: R) => U): Either<L, U> {
return Either.right<L, U>(f(this.value));
}
isLeft(): boolean { return false; }
isRight(): boolean { return true; }
getRight(): R { return this.value; }
toString(): string { return `Right(${this.value})`; }
}
// Array Monad Implementation
class ArrayMonad<T> implements Monad<T> {
constructor(private values: T[]) {}
static pure<T>(value: T): ArrayMonad<T> {
return new ArrayMonad([value]);
}
bind<U>(f: (value: T) => ArrayMonad<U>): ArrayMonad<U> {
const results: U[] = [];
for (const value of this.values) {
results.push(...f(value).getValues());
}
return new ArrayMonad(results);
}
map<U>(f: (value: T) => U): ArrayMonad<U> {
return new ArrayMonad(this.values.map(f));
}
getValues(): T[] { return this.values; }
toString(): string { return `Array([${this.values.join(', ')}])`; }
}
// Example usage
function safeDiv(x: number, y: number): Maybe<number> {
return y === 0 ? Maybe.none<number>() : Maybe.pure(x / y);
}
function parseNumber(str: string): Either<string, number> {
const num = parseFloat(str);
return isNaN(num) ?
Either.left<string, number>(`Cannot parse '${str}' as number`) :
Either.right<string, number>(num);
}
// Maybe monad calculation
const maybeResult = Maybe.pure(10)
.bind(x => Maybe.pure(2).bind(y => safeDiv(x, y)))
.map(z => z * 3);
console.log(maybeResult.toString()); // Some(15)
// Either monad calculation
const eitherResult = parseNumber("10")
.bind(x => parseNumber("2").bind(y =>
y === 0 ? Either.left<string, number>("Division by zero") :
Either.right<string, number>(x / y)))
.map(z => z * 3);
console.log(eitherResult.toString()); // Right(15)
// Array monad (non-deterministic computation)
const arrayResult = ArrayMonad.pure(1)
.bind(x => new ArrayMonad([x, x + 1, x + 2]))
.bind(y => new ArrayMonad([y * 2, y * 3]))
.map(z => z + 10);
console.log(arrayResult.toString()); // Array([12, 13, 14, 16, 18, 20])
C# integrates monads through LINQ's SelectMany method, providing natural do-notation:
using System;
using System.Collections.Generic;
using System.Linq;
// Generic Monad interface
public interface IMonad<T>
{
IMonad<U> Bind<U>(Func<T, IMonad<U>> f);
IMonad<U> Map<U>(Func<T, U> f);
}
// Maybe Monad Implementation
public abstract class Maybe<T> : IMonad<T>
{
public abstract IMonad<U> Bind<U>(Func<T, IMonad<U>> f);
public abstract IMonad<U> Map<U>(Func<T, U> f);
public abstract bool IsSome { get; }
public abstract bool IsNone { get; }
// LINQ Support
public abstract Maybe<U> SelectMany<U>(Func<T, Maybe<U>> f);
public abstract Maybe<V> SelectMany<U, V>(Func<T, Maybe<U>> f, Func<T, U, V> projection);
public abstract Maybe<U> Select<U>(Func<T, U> f);
public static Maybe<T> Pure(T value) => new Some<T>(value);
public static Maybe<T> None() => new None<T>();
}
public class Some<T> : Maybe<T>
{
private readonly T value;
public Some(T value) { this.value = value; }
public override IMonad<U> Bind<U>(Func<T, IMonad<U>> f) => f(value);
public override IMonad<U> Map<U>(Func<T, U> f) => Maybe<U>.Pure(f(value));
public override Maybe<U> SelectMany<U>(Func<T, Maybe<U>> f) => f(value);
public override Maybe<V> SelectMany<U, V>(Func<T, Maybe<U>> f, Func<T, U, V> projection)
{
return f(value).SelectMany(u => Maybe<V>.Pure(projection(value, u)));
}
public override Maybe<U> Select<U>(Func<T, U> f) => Maybe<U>.Pure(f(value));
public override bool IsSome => true;
public override bool IsNone => false;
public T Value => value;
public override string ToString() => $"Some({value})";
}
public class None<T> : Maybe<T>
{
public override IMonad<U> Bind<U>(Func<T, IMonad<U>> f) => Maybe<U>.None();
public override IMonad<U> Map<U>(Func<T, U> f) => Maybe<U>.None();
public override Maybe<U> SelectMany<U>(Func<T, Maybe<U>> f) => Maybe<U>.None();
public override Maybe<V> SelectMany<U, V>(Func<T, Maybe<U>> f, Func<T, U, V> projection)
{
return Maybe<V>.None();
}
public override Maybe<U> Select<U>(Func<T, U> f) => Maybe<U>.None();
public override bool IsSome => false;
public override bool IsNone => true;
public override string ToString() => "None";
}
// Either Monad Implementation
public abstract class Either<L, R> : IMonad<R>
{
public abstract IMonad<U> Bind<U>(Func<R, IMonad<U>> f);
public abstract IMonad<U> Map<U>(Func<R, U> f);
// LINQ Support
public abstract Either<L, U> SelectMany<U>(Func<R, Either<L, U>> f);
public abstract Either<L, V> SelectMany<U, V>(Func<R, Either<L, U>> f, Func<R, U, V> projection);
public abstract Either<L, U> Select<U>(Func<R, U> f);
public static Either<L, R> Left(L value) => new Left<L, R>(value);
public static Either<L, R> Right(R value) => new Right<L, R>(value);
}
public class Left<L, R> : Either<L, R>
{
private readonly L value;
public Left(L value) { this.value = value; }
public override IMonad<U> Bind<U>(Func<R, IMonad<U>> f) =>
new Left<L, U>(value) as IMonad<U>;
public override IMonad<U> Map<U>(Func<R, U> f) =>
new Left<L, U>(value) as IMonad<U>;
public override Either<L, U> SelectMany<U>(Func<R, Either<L, U>> f) =>
Either<L, U>.Left(value);
public override Either<L, V> SelectMany<U, V>(Func<R, Either<L, U>> f, Func<R, U, V> projection) =>
Either<L, V>.Left(value);
public override Either<L, U> Select<U>(Func<R, U> f) => Either<L, U>.Left(value);
public L Value => value;
public override string ToString() => $"Left({value})";
}
public class Right<L, R> : Either<L, R>
{
private readonly R value;
public Right(R value) { this.value = value; }
public override IMonad<U> Bind<U>(Func<R, IMonad<U>> f) => f(value);
public override IMonad<U> Map<U>(Func<R, U> f) => Either<L, U>.Right(f(value)) as IMonad<U>;
public override Either<L, U> SelectMany<U>(Func<R, Either<L, U>> f) => f(value);
public override Either<L, V> SelectMany<U, V>(Func<R, Either<L, U>> f, Func<R, U, V> projection)
{
return f(value).SelectMany(u => Either<L, V>.Right(projection(value, u)));
}
public override Either<L, U> Select<U>(Func<R, U> f) => Either<L, U>.Right(f(value));
public R Value => value;
public override string ToString() => $"Right({value})";
}
// Extension methods for IEnumerable (List Monad)
public static class ListMonadExtensions
{
public static IEnumerable<U> SelectMany<T, U>(
this IEnumerable<T> source,
Func<T, IEnumerable<U>> selector)
{
return source.SelectMany(selector);
}
}
class Program
{
static Maybe<double> SafeDiv(double x, double y)
{
return y == 0 ? Maybe<double>.None() : Maybe<double>.Pure(x / y);
}
static Either<string, double> ParseNumber(string str)
{
return double.TryParse(str, out var result) ?
Either<string, double>.Right(result) :
Either<string, double>.Left($"Cannot parse '{str}' as number");
}
static void Main()
{
// Maybe monad with LINQ syntax
var maybeResult =
from x in Maybe<double>.Pure(10)
from y in Maybe<double>.Pure(2)
from z in SafeDiv(x, y)
select z * 3;
Console.WriteLine($"Maybe result: {maybeResult}"); // Some(15)
// Either monad with LINQ syntax
var eitherResult =
from x in ParseNumber("10")
from y in ParseNumber("2")
from z in (y == 0 ? Either<string, double>.Left("Division by zero") :
Either<string, double>.Right(x / y))
select z * 3;
Console.WriteLine($"Either result: {eitherResult}"); // Right(15)
// List monad (built into LINQ)
var listResult =
from x in new[] { 1, 2, 3 }
from y in new[] { 10, 20 }
select x + y;
Console.WriteLine($"List result: [{string.Join(", ", listResult)}]");
// [11, 21, 12, 22, 13, 23]
// Nested computation with error handling
var complexResult =
from a in ParseNumber("10")
from b in ParseNumber("2")
from c in SafeDiv(a, b).IsSome ?
Either<string, double>.Right(((Some<double>)SafeDiv(a, b)).Value) :
Either<string, double>.Left("Division failed")
from d in ParseNumber("3")
select c * d + 1;
Console.WriteLine($"Complex result: {complexResult}"); // Right(16)
}
}
Fluent builder pattern #
There is one more pattern that is worth mentioning. The fluent builder pattern[2][3] provides a way to compose monadic operations through method chaining, creating a domain-specific language (DSL) for sequential computations. This pattern wraps monadic values in a builder class that exposes fluent methods like bind, map, and run, allowing developers to express complex monadic workflows in a readable syntax.
At its core, the fluent builder pattern leverages the insight that monads can present a builder or fluent interface for wrapped values. When you call bind on a builder, the return value becomes the new wrapped value that the next method in the chain operates on. This creates a pipeline where each step can transform, validate, or potentially short-circuit the computation.
This approach is particularly valuable in scenarios involving multiple sequential operations that might fail, such as data validation pipelines, API call sequences, or complex computations where each step depends on the previous result.
// Maybe type definition
type Maybe<T> = T | null;
// Helper functions for Maybe monad
function pure<T>(value: T): Maybe<T> {
return value;
}
function flatMap<A, B>(ma: Maybe<A>, f: (a: A) => Maybe<B>): Maybe<B> {
if (ma === null) {
return null;
}
return f(ma);
}
function safeDiv(x: number, y: number): Maybe<number> {
return y === 0 ? null : x / y;
}
// Fluent builder pattern for monadic computations
class DoBuilder<T> {
constructor(private value: Maybe<T>) {}
static from<T>(value: Maybe<T>): DoBuilder<T> {
return new DoBuilder(value);
}
static pure<T>(value: T): DoBuilder<T> {
return new DoBuilder(pure(value));
}
bind<U>(f: (value: T) => Maybe<U>): DoBuilder<U> {
return new DoBuilder(flatMap(this.value, f));
}
map<U>(f: (value: T) => U): DoBuilder<U> {
return new DoBuilder(
this.value !== null ? f(this.value) : null
);
}
run(): Maybe<T> {
return this.value;
}
}
// Usage examples:
const builderExample1 = DoBuilder
.pure(20)
.bind(x => safeDiv(x, 4))
.bind(y => safeDiv(y, 2))
.map(z => z * 3)
.run(); // Result: 7.5
const builderExample2 = DoBuilder
.pure(20)
.bind(x => safeDiv(x, 0)) // This will fail
.bind(y => safeDiv(y, 2))
.map(z => z * 3)
.run(); // Result: null
// More complex example with string parsing
function parseNumber(str: string): Maybe<number> {
const num = parseFloat(str);
return isNaN(num) ? null : num;
}
const complexCalculation = DoBuilder
.from(parseNumber("10"))
.bind(x => DoBuilder.from(parseNumber("2")).run())
.bind(y => safeDiv(10, y)) // Using the first parsed number
.map(result => `Result: ${result}`)
.run(); // Result: "Result: 5"
console.log("Example 1:", builderExample1); // 7.5
console.log("Example 2:", builderExample2); // null
console.log("Complex:", complexCalculation); // "Result: 5"
// Builder pattern with error context
class DoBuilderWithError<T> {
constructor(
private value: Maybe<T>,
private errorContext: string[] = []
) {}
static pure<T>(value: T): DoBuilderWithError<T> {
return new DoBuilderWithError(pure(value));
}
bind<U>(
f: (value: T) => Maybe<U>,
errorMsg?: string
): DoBuilderWithError<U> {
if (this.value === null) {
return new DoBuilderWithError<U>(null, this.errorContext);
}
const result = f(this.value);
const newContext = result === null && errorMsg
? [...this.errorContext, errorMsg]
: this.errorContext;
return new DoBuilderWithError(result, newContext);
}
map<U>(f: (value: T) => U): DoBuilderWithError<U> {
return new DoBuilderWithError(
this.value !== null ? f(this.value) : null,
this.errorContext
);
}
run(): { value: Maybe<T>; errors: string[] } {
return { value: this.value, errors: this.errorContext };
}
}
// Usage with error tracking:
const resultWithErrors = DoBuilderWithError
.pure(10)
.bind(x => safeDiv(x, 0), "Division by zero in step 1")
.bind(y => safeDiv(y, 2), "Division failed in step 2")
.map(z => z * 3)
.run();
console.log("Result with errors:", resultWithErrors);
// { value: null, errors: ["Division by zero in step 1"] }
using System;
using System.Collections.Generic;
using System.Linq;
// Maybe type definition for C#
public class Maybe<T>
{
private readonly T? value;
private readonly bool hasValue;
private Maybe(T? value, bool hasValue)
{
this.value = value;
this.hasValue = hasValue;
}
public static Maybe<T> Pure(T value) => new Maybe<T>(value, true);
public static Maybe<T> None() => new Maybe<T>(default(T), false);
public Maybe<U> Bind<U>(Func<T, Maybe<U>> f)
{
return hasValue && value != null ? f(value) : Maybe<U>.None();
}
public Maybe<U> Map<U>(Func<T, U> f)
{
return hasValue && value != null ? Maybe<U>.Pure(f(value)) : Maybe<U>.None();
}
// LINQ Support - Required for query syntax
public Maybe<U> SelectMany<U>(Func<T, Maybe<U>> selector)
{
return Bind(selector);
}
public Maybe<V> SelectMany<U, V>(Func<T, Maybe<U>> selector, Func<T, U, V> resultSelector)
{
return Bind(x => selector(x).Bind(y => Maybe<V>.Pure(resultSelector(x, y))));
}
public Maybe<U> Select<U>(Func<T, U> selector)
{
return Map(selector);
}
public bool HasValue => hasValue && value != null;
public T? Value => hasValue ? value : default(T);
public bool IsNone => !hasValue || value == null;
public override string ToString()
{
return HasValue ? $"Some({Value})" : "None";
}
public override bool Equals(object? obj)
{
if (obj is Maybe<T> other)
{
if (!HasValue && !other.HasValue) return true;
if (HasValue && other.HasValue) return EqualityComparer<T>.Default.Equals(Value, other.Value);
}
return false;
}
public override int GetHashCode()
{
return HasValue ? EqualityComparer<T>.Default.GetHashCode(Value!) : 0;
}
}
// Helper functions for Maybe monad
public static class MaybeHelpers
{
public static Maybe<T> Pure<T>(T value) => Maybe<T>.Pure(value);
public static Maybe<U> FlatMap<T, U>(Maybe<T> ma, Func<T, Maybe<U>> f)
{
return ma.Bind(f);
}
public static Maybe<double> SafeDiv(double x, double y)
{
return y == 0 ? Maybe<double>.None() : Maybe<double>.Pure(x / y);
}
public static Maybe<double> ParseNumber(string str)
{
return double.TryParse(str, out var result) ?
Maybe<double>.Pure(result) :
Maybe<double>.None();
}
}
// Fluent builder pattern for monadic computations
public class DoBuilder<T>
{
private readonly Maybe<T> value;
private DoBuilder(Maybe<T> value)
{
this.value = value;
}
public static DoBuilder<T> From(Maybe<T> value)
{
return new DoBuilder<T>(value);
}
public static DoBuilder<T> Pure(T value)
{
return new DoBuilder<T>(Maybe<T>.Pure(value));
}
public DoBuilder<U> Bind<U>(Func<T, Maybe<U>> f)
{
return new DoBuilder<U>(value.Bind(f));
}
public DoBuilder<U> Map<U>(Func<T, U> f)
{
return new DoBuilder<U>(value.Map(f));
}
public Maybe<T> Run()
{
return value;
}
// Helper methods
public bool IsNone => value.IsNone;
public T? GetValue() => value.Value;
}
// Builder pattern with error context
public class DoBuilderWithError<T>
{
private readonly Maybe<T> value;
private readonly List<string> errorContext;
private DoBuilderWithError(Maybe<T> value, List<string>? errorContext = null)
{
this.value = value;
this.errorContext = errorContext ?? new List<string>();
}
public static DoBuilderWithError<T> Pure(T value)
{
return new DoBuilderWithError<T>(Maybe<T>.Pure(value));
}
public DoBuilderWithError<U> Bind<U>(Func<T, Maybe<U>> f, string? errorMsg = null)
{
if (value.IsNone)
{
return new DoBuilderWithError<U>(Maybe<U>.None(), errorContext);
}
var result = f(value.Value!);
var newContext = new List<string>(errorContext);
if (result.IsNone && !string.IsNullOrEmpty(errorMsg))
{
newContext.Add(errorMsg);
}
return new DoBuilderWithError<U>(result, newContext);
}
public DoBuilderWithError<U> Map<U>(Func<T, U> f)
{
var newValue = value.HasValue ? Maybe<U>.Pure(f(value.Value!)) : Maybe<U>.None();
return new DoBuilderWithError<U>(newValue, errorContext);
}
public (Maybe<T> Value, List<string> Errors) Run()
{
return (value, new List<string>(errorContext));
}
}
class Program
{
static void Main()
{
// Usage examples:
var builderExample1 = DoBuilder<double>
.Pure(20.0)
.Bind(x => MaybeHelpers.SafeDiv(x, 4))
.Bind(y => MaybeHelpers.SafeDiv(y, 2))
.Map(z => z * 3)
.Run(); // Result: 7.5
var builderExample2 = DoBuilder<double>
.Pure(20.0)
.Bind(x => MaybeHelpers.SafeDiv(x, 0)) // This will fail
.Bind(y => MaybeHelpers.SafeDiv(y, 2))
.Map(z => z * 3)
.Run(); // Result: None
// More complex example with string parsing
var complexCalculation = DoBuilder<string>
.From(MaybeHelpers.ParseNumber("10").Map(x => x.ToString()))
.Bind(x => MaybeHelpers.ParseNumber("2").Map(y => $"Division of {x} by {y}"))
.Bind(desc => MaybeHelpers.SafeDiv(10, 2).Map(result => $"{desc} = {result}"))
.Run();
Console.WriteLine($"Example 1: {builderExample1}"); // Some(7.5)
Console.WriteLine($"Example 2: {builderExample2}"); // None
Console.WriteLine($"Complex: {complexCalculation}"); // Some(Division of 10 by 2 = 5)
// Usage with error tracking:
var resultWithErrors = DoBuilderWithError<double>
.Pure(10.0)
.Bind(x => MaybeHelpers.SafeDiv(x, 0), "Division by zero in step 1")
.Bind(y => MaybeHelpers.SafeDiv(y, 2), "Division failed in step 2")
.Map(z => z * 3)
.Run();
Console.WriteLine($"Result with errors: Value = {resultWithErrors.Value}, " +
$"Errors = [{string.Join(", ", resultWithErrors.Errors.Select(e => $"\"{e}\""))}]");
// Result with errors: Value = None, Errors = ["Division by zero in step 1"]
// LINQ-style usage with proper SelectMany support
var linqResult =
from x in Maybe<double>.Pure(20.0)
from y in MaybeHelpers.SafeDiv(x, 4)
from z in MaybeHelpers.SafeDiv(y, 2)
select z * 3;
Console.WriteLine($"LINQ result: {linqResult}"); // Some(7.5)
// Demonstrating error propagation with LINQ
var linqErrorResult =
from x in Maybe<double>.Pure(10.0)
from y in MaybeHelpers.SafeDiv(x, 0) // This fails
from z in MaybeHelpers.SafeDiv(y, 2)
select z * 3;
Console.WriteLine($"LINQ error result: {linqErrorResult}"); // None
// Complex chaining example
var chainedResult = DoBuilder<double>
.Pure(100.0)
.Bind(x => MaybeHelpers.SafeDiv(x, 5)) // 20
.Bind(y => MaybeHelpers.SafeDiv(y, 4)) // 5
.Bind(z => MaybeHelpers.SafeDiv(z, 1)) // 5
.Map(final => final * 2) // 10
.Run();
Console.WriteLine($"Chained result: {chainedResult}"); // Some(10)
}
}
Applicatives #
As you can see, in Haskell a monad is built on top of an applicative. Writer applicative was just one illustration to prove the point. But there is a deeper connection between monads and applicatives. And to see it properly, we need to explore hidden applicative secrets.
Applicative as Lax Monoidal Functor #
In category theory, an applicative functor is commonly described as a lax monoidal functor with tensorial strength. This means that it is a functor equipped with coherent ways to lift the monoidal unit and combine independent effectful values. In the programming examples here, the monoidal structure is the product of values.
A functor F : C -> D is lax monoidal if it comes equipped with:
- A unit map:
η : I_D -> F(I_C), whereI_CandI_Dare the unit objects of the monoidal categories. - A product map:
μ_{A,B} : F(A) ⊗ F(B) -> F(A ⊗ B)
These must satisfy coherence laws with respect to associativity and unit of the monoidal structures.
In Haskell:
- The monoidal structure is the cartesian product
(×). - The lax monoidal structure can be expressed with:
unit :: F ()pair :: F a -> F b -> F (a, b)
- The usual applicative operations are equivalent:
pure :: a -> F acan be recovered fromunitandfmapliftA2 :: (a -> b -> c) -> F a -> F b -> F crecovers the product map(<*>) :: F (a -> b) -> F a -> F bis the function-application form
In this setting, an applicative functor corresponds to a strong lax monoidal endofunctor on
(Set, ×, 1).
F A × F B --- μ ---> F(A × B)
| |
| |
v v
F f × F g =========> F(f × g)
This means:
- Start with independent effectful values
F AandF B. - Combine them into a pair inside the functor
F (A × B). - Naturality ensures functions
f : A -> A'andg : B -> B'behave consistently.
Tensorial Strength #
A strong lax monoidal functor also has strength: a way to let ordinary values interact with effectful ones.
For cartesian products, this has the shape:
strength :: (a, F b) -> F (a, b)
Applicative programming usually exposes the related operations:
(<*>) :: F (a -> b) -> F a -> F b
liftA2 :: (a -> b -> c) -> F a -> F b -> F c
These operations combine independent effectful values. The strength is the categorical ingredient that lets ordinary values and effectful values fit together coherently.
Monads as Stronger Applicatives #
In Haskell and similar programming settings, every monad gives an applicative structure, but not every applicative is a monad. Categorically:
-
Applicatives combine independent effectful values using coherent monoidal structure.
-
Monads add the ability to flatten nested contexts (
join :: m (m a) -> m a), enabling sequencing of dependent computations. -
Applicative: you can combine independent computations:
liftA2 (+) (Just 3) (Just 4). -
Monad: you can feed the output of one computation into another:
do { x <- Just 3; y <- Just 4; return (x+y) }.
Monads extend applicative-style independent composition with sequential composition (
>>=), where later computations can depend on earlier results.
Triangle monad example #
Suppose we have a category of triangles where monadic operations enable sequential geometric transformations with state management.
- Objects - Triangles in the plane with additional state (history, metadata)
- Morphisms - Geometric transformations that may fail or accumulate state
A monad M on this category of triangles provides:
- Type Constructor:
M(T)wraps a triangleTwith computational context (success/failure, transformation history, accumulated state) - Unit/Return:
η: T → M(T)lifts a plain triangle into the monadic context - Bind:
(>>=): M(T) → (T → M(T')) → M(T')sequences transformations while managing state and potential failure - Join:
μ_T: M(M(T)) → M(T)flattens nested transformation contexts
Unlike applicative functors, which combine transformations whose structure is known independently, monads enable sequential dependent transformations where each step can:
- Inspect the result of the previous transformation
- Accumulate transformation history (e.g., rotation angles, translation vectors)
- Short-circuit on failure (e.g., invalid transformations, boundary violations)
- Pass state between transformations (e.g., coordinate system changes, constraint violations)
Consider a monad TransformM that tracks transformation history and validates geometric constraints:
TransformM(T) = {
triangle: T,
history: [Transformation],
isValid: boolean,
constraints: ConstraintSet
}
1. Unit Operation (return/pure)
η: Triangle → TransformM(Triangle)
η(T) = TransformM {
triangle: T,
history: [],
isValid: true,
constraints: defaultConstraints
}
2. Bind Operation (>>=)
(>>=): TransformM(T) → (T → TransformM(T')) → TransformM(T')
m >>= f = case m of
TransformM { triangle: T, history: H, isValid: false, ... } →
TransformM { triangle: invalid, history: H, isValid: false, ... }
TransformM { triangle: T, history: H, isValid: true, constraints: C } →
let result = f(T)
newHistory = H ++ [currentTransform]
constraintCheck = validateConstraints(result.triangle, C)
in TransformM {
triangle: result.triangle,
history: newHistory,
isValid: result.isValid && constraintCheck,
constraints: result.constraints
}
3. Sequential Transformation Pipeline
Initial Triangle T₀
|
| η (unit)
▼
TransformM(T₀) { triangle: T₀, history: [], isValid: true }
|
| >>= rotate(45°)
▼
TransformM(T₁) { triangle: T₁, history: [Rotate(45°)], isValid: true }
|
| >>= translate(10, 5)
▼
TransformM(T₂) { triangle: T₂, history: [Rotate(45°), Translate(10,5)], isValid: true }
|
| >>= scale(2.0)
▼
TransformM(T₃) { triangle: T₃, history: [Rotate(45°), Translate(10,5), Scale(2.0)], isValid: true }
|
| >>= validateBounds
▼
TransformM(T₃) { triangle: T₃, history: [...], isValid: checkBounds(T₃) }
4. Error Propagation: If any transformation fails (e.g., results in degenerate triangle),
all subsequent transformations are skipped:
triangle >>= rotate(45°) >>= invalidScale(0) >>= translate(10,5)
↑
failure here stops the chain
5. State Accumulation: Each transformation can modify the constraint set or accumulated metadata:
triangle >>= addConstraint(maxArea(100))
>>= scale(2.0) -- might fail if area exceeds constraint
>>= rotate(90°)
6. History Tracking: The complete transformation sequence is preserved for debugging, undo operations, or optimization:
getHistory(finalResult) = [Rotate(45°), Scale(2.0), Translate(10,5), Reflect(xAxis)]
Key Differences from Applicative #
| Aspect | Applicative Functor | Monad |
|---|---|---|
| Dependency | Independent transformations | Sequential dependent transformations |
| State | Fixed structure known up front | State can be threaded between steps |
| Failure | Combines independent failures according to the instance | Can choose the next step only after success |
| History | Can accumulate independent history | Can let history influence later steps |
| Constraints | Static constraints | Dynamic constraint evolution |
Monadic Laws in Triangle Context #
-
Left Identity:
η(T) >>= f = f(T)- Lifting a triangle and applying a transformation equals applying the transformation directly
-
Right Identity:
m >>= η = m- Binding with the unit operation preserves the monadic triangle
-
Associativity:
(m >>= f) >>= g = m >>= (\x -> f(x) >>= g)- Regrouping the binding transformations does not affect the final result
Visualizing monads #
A monad M builds upon functor and applicative with three essential components:
- Type Constructor
M- Wraps values in computational context (error handling, state, etc.) - Unit/Return
η: A -> M(A)- Lifts pure values into the monadic context - Bind
(>>=): M(A) -> (A -> M(B)) -> M(B)- Chains computations that produce wrapped values
1. Sequential dependent computations
Inside one category:
Objects: A, B, C and their wrapped forms M(A), M(B), M(C)
Morphisms: ordinary arrows A -> B, functor-mapped arrows M(A) -> M(B), and arrows A -> M(B)
Sequential Chain:
A ----f----> B ----g----> C
Monadic Chain:
M(A) ----bind(f')----> M(B) ----bind(g')----> M(C)
where:
- f': A -> M(B) (function that produces wrapped result)
- g': B -> M(C) (function that produces wrapped result)
2. Dependent vs Independent Computation
Applicative (Independent):
F(A) F(B) F(C)
| | |
| | | Independent computations
| | | can be combined without depending on one another
v v v
liftA3 h F(A) F(B) F(C) -----> F(result)
Monad (Sequential):
M(A) --bind--> M(B) --bind--> M(C) --bind--> M(result)
| | | |
| | | |
└─── f ───────┴─── g ────────┴─── h ────────┘
3. Monad Laws Visualization
- Left Identity: return a >>= f = f a
a
|
| return/pure
v
M(a) ------bind(f)------> M(b)
equals
a --------f---------> M(b)
- Right Identity: m >>= return = m
M(a) ------bind(return)------> M(a)
equals
M(a) --------identity--------> M(a)
- Associativity: (m >>= f) >>= g = m >>= (\x -> f x >>= g)
Left side:
M(a) --bind(f)--> M(b) --bind(g)--> M(c)
Right side:
M(a) --bind(\x -> f(x) >>= g)--> M(c)
4. Monadic Context Flow
Pure World Monadic World
A M(A)
| |
| computation | bind
v v
B M(B)
| |
| computation | bind
v v
C M(C)
Context preserved and threaded through each step
5. Error Handling Example (Maybe Monad)
Just 10 --bind--> safeDivide(_, 2) --bind--> Just 5 --bind--> multiplyBy(_, 3) --> Just 15
Just 10 --bind--> safeDivide(_, 0) --bind--> Nothing ----bind----> Nothing --> Nothing
| |
| |
Short-circuits Skips computation
6. State Threading Example
Initial State: S₀
computation₁: A -> State S₀ (B, S₁)
| |
v v
M(A,S₀) -----> M(B,S₁)
|
| bind with computation₂
v
M(C,S₂) -----> M(D,S₃)
|
| final result
v
(D, S₃)
State is automatically threaded through the computation chain
Comparison Summary #
| Aspect | Functor | Applicative | Monad |
|---|---|---|---|
| Operation | map: (A -> B) -> F(A) -> F(B) |
apply: F(A -> B) -> F(A) -> F(B) |
bind: M(A) -> (A -> M(B)) -> M(B) |
| Dependency | Transform values | Independent effects | Sequential dependencies |
| Context | Preserve context | Combine contexts | Transform context |
| Use Case | Simple transformations | Independent validation/combination | Error chains, state |
Initial problem #
Finally we can get back to out original user request problem. And this is how the solution looks like now (feel the difference):
// Types and interfaces for better type safety
interface User {
id: number;
name: string;
}
interface UserPermissions {
canAccess: boolean;
}
interface ProcessedData {
processed: boolean;
data: any;
}
interface SavedData {
saved: boolean;
data: any;
}
interface SuccessResult {
success: SavedData;
}
interface ErrorResult {
error: string;
}
type Result = SuccessResult | ErrorResult;
const database = {
getUser: (userId: number): User | null => {
// Simulate a user fetch
return userId > 0 ? { id: userId, name: "John Doe" } : null;
}
};
const auth = {
checkPermissions: (user: User): UserPermissions | null => {
// Simulate permission check
return user ? { canAccess: true } : null;
}
};
const validator = {
validate: (data: any): any | null => {
// Simulate data validation
return data && typeof data === 'object' ? data : null;
}
};
const processor = {
process: (data: any): ProcessedData | null => {
// Simulate data processing
return data ? { processed: true, data } : null;
}
};
const storage = {
save: (data: ProcessedData): SavedData | null => {
// Simulate data storage
return data ? { saved: true, data } : null;
}
};
// Example of the monadic approach (as mentioned in the introduction)
class Maybe<T> {
constructor(private value: T | null) {}
static of<T>(value: T | null): Maybe<T> {
return new Maybe(value);
}
flatMap<U>(fn: (value: T) => Maybe<U>): Maybe<U> {
if (this.value === null) {
return new Maybe<U>(null);
}
return fn(this.value);
}
map<U>(fn: (value: T) => U): Maybe<U> {
if (this.value === null) {
return new Maybe<U>(null);
}
return Maybe.of(fn(this.value));
}
getValue(): T | null {
return this.value;
}
isNothing(): boolean {
return this.value === null;
}
}
// Monadic version of the same function
function processUserRequestMonadic(userId: number, requestData: any): Maybe<SavedData> {
return Maybe.of(database.getUser(userId))
.flatMap(user => Maybe.of(auth.checkPermissions(user)))
.flatMap(permissions => Maybe.of(validator.validate(requestData)))
.flatMap(validatedData => Maybe.of(processor.process(validatedData)))
.flatMap(result => Maybe.of(storage.save(result)));
}
// Usage of monadic version
const monadicResult = processUserRequestMonadic(1, { key: "value" });
if (monadicResult.isNothing()) {
console.log("Operation failed at some step");
} else {
console.log("Success:", monadicResult.getValue());
}
CSV Transform example #
Our CSV transformation pipeline benefits from monadic composition as well.
The monadic approach transforms our transformation pipeline from independent operations to a dependent chain where:
- 🔗 Sequential Processing: Each transformation receives the result of the previous step
- 🚨 Early Termination: Invalid data immediately stops the pipeline
- 📝 State Tracking: Accumulate transformation history and validation results
- 🎯 Contextual Logic: Later transformations can inspect earlier results
// Result monad for error handling
interface Result<T, E> {
bind<U>(f: (value: T) => Result<U, E>): Result<U, E>;
map<U>(f: (value: T) => U): Result<U, E>;
isOk(): boolean;
isError(): boolean;
}
class ResultFactory {
static ok<T, E>(value: T): Result<T, E> {
return new Ok(value);
}
static error<T, E>(error: E): Result<T, E> {
return new ErrorResult(error);
}
}
class Ok<T, E> implements Result<T, E> {
constructor(private value: T) {}
bind<U>(f: (value: T) => Result<U, E>): Result<U, E> {
return f(this.value);
}
map<U>(f: (value: T) => U): Result<U, E> {
return ResultFactory.ok<U, E>(f(this.value));
}
isOk(): boolean { return true; }
isError(): boolean { return false; }
getValue(): T { return this.value; }
}
class ErrorResult<T, E> implements Result<T, E> {
constructor(private error: E) {}
bind<U>(f: (value: T) => Result<U, E>): Result<U, E> {
return new ErrorResult<U, E>(this.error);
}
map<U>(f: (value: T) => U): Result<U, E> {
return new ErrorResult<U, E>(this.error);
}
isOk(): boolean { return false; }
isError(): boolean { return true; }
getError(): E { return this.error; }
}
// Configuration and types
type Config = {
ageThreshold: number;
locale: 'en' | 'de';
requireValidAge: boolean;
};
type Row = Record<string, string>;
type TransformationError =
| { type: 'invalid_age'; value: string }
| { type: 'missing_field'; field: string }
| { type: 'age_out_of_range'; age: number };
type Person = {
name: string;
age: number;
ageGroup: string;
transformationHistory: string[];
};
// Reader monad for configuration dependency
class ReaderResult<R, T, E> {
constructor(private run: (config: R) => Result<T, E>) {}
static pure<R, T, E>(value: T): ReaderResult<R, T, E> {
return new ReaderResult(() => ResultFactory.ok<T, E>(value));
}
static error<R, T, E>(error: E): ReaderResult<R, T, E> {
return new ReaderResult(() => ResultFactory.error<T, E>(error));
}
bind<U>(f: (value: T) => ReaderResult<R, U, E>): ReaderResult<R, U, E> {
return new ReaderResult((config: R) => {
const result = this.run(config);
if (result.isError()) {
return ResultFactory.error<U, E>((result as ErrorResult<T, E>).getError());
}
return f((result as Ok<T, E>).getValue()).run(config);
});
}
map<U>(f: (value: T) => U): ReaderResult<R, U, E> {
return new ReaderResult((config: R) => {
const result = this.run(config);
return result.map(f);
});
}
execute(config: R): Result<T, E> {
return this.run(config);
}
}
// Monadic transformation pipeline
function parseAge(rawAge: string): ReaderResult<Config, number, TransformationError> {
return new ReaderResult((config: Config) => {
const age = parseInt(rawAge, 10);
if (isNaN(age)) {
return ResultFactory.error<number, TransformationError>({
type: 'invalid_age',
value: rawAge
});
}
if (config.requireValidAge && (age < 0 || age > 150)) {
return ResultFactory.error<number, TransformationError>({
type: 'age_out_of_range',
age
});
}
return ResultFactory.ok<number, TransformationError>(age);
});
}
function categorizeAge(age: number): ReaderResult<Config, string, TransformationError> {
return new ReaderResult((config: Config) => {
const ageGroup = age > config.ageThreshold ? 'old' : 'young';
return ResultFactory.ok<string, TransformationError>(ageGroup);
});
}
function localizeName(name: string): ReaderResult<Config, string, TransformationError> {
return new ReaderResult((config: Config) => {
const localizedName = config.locale === 'de'
? name.toUpperCase()
: name.toLowerCase();
return ResultFactory.ok<string, TransformationError>(localizedName);
});
}
// Monadic composition - each step depends on the previous
function transformRow(row: Row): ReaderResult<Config, Person, TransformationError> {
const history: string[] = [];
if (!row.name) {
return ReaderResult.error({ type: 'missing_field', field: 'name' });
}
if (!row.age) {
return ReaderResult.error({ type: 'missing_field', field: 'age' });
}
// Sequential monadic chain - each step depends on previous results
return parseAge(row.age)
.bind(age => {
history.push(`Parsed age: ${age}`);
return categorizeAge(age)
.bind(ageGroup => {
history.push(`Categorized as: ${ageGroup}`);
return localizeName(row.name)
.map(localizedName => {
history.push(`Localized name: ${localizedName}`);
return {
name: localizedName,
age,
ageGroup,
transformationHistory: [...history]
};
});
});
});
}
// CSV processing with monadic error handling
function transformCSVData(csvData: Row[], config: Config): Result<Person[], TransformationError[]> {
const results: Person[] = [];
const errors: TransformationError[] = [];
for (const row of csvData) {
const result = transformRow(row).execute(config);
if (result.isOk()) {
results.push((result as Ok<Person, TransformationError>).getValue());
} else {
errors.push((result as ErrorResult<Person, TransformationError>).getError());
// In monadic approach, we can choose to continue or stop on first error
// Here we continue to collect all errors, but could short-circuit instead
}
}
return errors.length > 0
? ResultFactory.error<Person[], TransformationError[]>(errors)
: ResultFactory.ok<Person[], TransformationError[]>(results);
}
// Helper function to parse CSV
function readCSV(data: string): Row[] {
const lines = data.trim().split(/\r?\n/);
if (lines.length < 2) return [];
const keys = lines[0].split(',').map(k => k.trim());
return lines.slice(1).map(line => {
const values = line.split(',').map(v => v.trim().replace(/^"|"$/g, ''));
return Object.fromEntries(keys.map((k, i) => [k, values[i] || '']));
});
}
// Usage example
const csvData = readCSV('name,age\nAlice,25\nBob,invalid\nCharlie,35');
const config: Config = {
ageThreshold: 30,
locale: 'de',
requireValidAge: true
};
const result = transformCSVData(csvData, config);
if (result.isOk()) {
console.log('Transformed data:', (result as Ok<Person[], TransformationError[]>).getValue());
} else {
console.log('Transformation errors:', (result as ErrorResult<Person[], TransformationError[]>).getError());
}
// Output shows how monadic composition handles errors:
// - Alice: Successfully transformed
// - Bob: Error caught at age parsing step, preventing further processing
// - Charlie: Successfully transformed with different age group
import Data.Char (toUpper, toLower)
import Text.Read (readMaybe)
-- Configuration for transformations
data Config = Config
{ ageThreshold :: Int
, locale :: String
, requireValidAge :: Bool
} deriving (Show)
-- Transformation errors
data TransformationError
= InvalidAge String
| MissingField String
| AgeOutOfRange Int
deriving (Show, Eq)
-- Result type after transformation
data Person = Person
{ personName :: String
, personAge :: Int
, personAgeGroup :: String
, transformationHistory :: [String]
} deriving (Show)
-- Simple Result type (similar to Either but more explicit)
data Result e a = Error e | Ok a deriving (Show, Eq)
instance Functor (Result e) where
fmap _ (Error e) = Error e
fmap f (Ok a) = Ok (f a)
instance Applicative (Result e) where
pure = Ok
Error e <*> _ = Error e
Ok f <*> something = fmap f something
instance Monad (Result e) where
return = pure
Error e >>= _ = Error e
Ok a >>= f = f a
-- Parse age with validation
parseAge :: Config -> String -> Result TransformationError Int
parseAge config ageStr =
case readMaybe ageStr of
Nothing -> Error (InvalidAge ageStr)
Just age ->
if requireValidAge config && (age < 0 || age > 150)
then Error (AgeOutOfRange age)
else Ok age
-- Categorize age based on config threshold
categorizeAge :: Config -> Int -> Result TransformationError String
categorizeAge config age =
Ok $ if age > ageThreshold config then "old" else "young"
-- Localize name based on config locale
localizeName :: Config -> String -> Result TransformationError String
localizeName config name =
Ok $ case locale config of
"de" -> map toUpper name
"en" -> map toLower name
_ -> name
-- Monadic transformation pipeline (simplified without ReaderT)
transformPerson :: Config -> [(String, String)] -> Result TransformationError Person
transformPerson config row = do
-- Extract required fields
name <- case lookup "name" row of
Nothing -> Error (MissingField "name")
Just n -> Ok n
ageStr <- case lookup "age" row of
Nothing -> Error (MissingField "age")
Just a -> Ok a
-- Sequential monadic composition
age <- parseAge config ageStr
ageGroup <- categorizeAge config age
localizedName <- localizeName config name
-- Build result with transformation history
return Person
{ personName = localizedName
, personAge = age
, personAgeGroup = ageGroup
, transformationHistory =
[ "Parsed age: " ++ show age
, "Categorized as: " ++ ageGroup
, "Localized name: " ++ localizedName
]
}
-- Process CSV data with monadic error handling
transformCSVData :: [[(String, String)]] -> Config -> Result [TransformationError] [Person]
transformCSVData rows config =
let results = map (transformPerson config) rows
(errors, successes) = partitionResults results
in if null errors
then Ok successes
else Error errors
where
partitionResults :: [Result e a] -> ([e], [a])
partitionResults = foldr partitionResult ([], [])
where
partitionResult (Error e) (errs, succs) = (e:errs, succs)
partitionResult (Ok a) (errs, succs) = (errs, a:succs)
-- Simple CSV parser
parseCSV :: String -> [[(String, String)]]
parseCSV csv =
case lines csv of
[] -> []
(header:rows) ->
let keys = splitOn ',' header
parseRow row = zip keys (splitOn ',' row)
in map parseRow rows
where
splitOn :: Char -> String -> [String]
splitOn delim = words . map (\c -> if c == delim then ' ' else c)
-- Example usage
main :: IO ()
main = do
let csvData = "name,age\nAlice,25\nBob,invalid\nCharlie,35"
rows = parseCSV csvData
config = Config { ageThreshold = 30, locale = "de", requireValidAge = True }
result = transformCSVData rows config
case result of
Error errors -> do
putStrLn "Transformation errors:"
mapM_ print errors
Ok people -> do
putStrLn "Transformed data:"
mapM_ print people
using System;
using System.Collections.Generic;
using System.Linq;
// Configuration
public class Config
{
public int AgeThreshold { get; set; }
public string Locale { get; set; } = "en";
public bool RequireValidAge { get; set; }
}
// Transformation errors
public interface ITransformationError
{
string Message { get; }
}
public class InvalidAgeError : ITransformationError
{
public string Value { get; }
public InvalidAgeError(string value) => Value = value;
public string Message => $"Invalid age: {Value}";
}
public class MissingFieldError : ITransformationError
{
public string Field { get; }
public MissingFieldError(string field) => Field = field;
public string Message => $"Missing field: {Field}";
}
public class AgeOutOfRangeError : ITransformationError
{
public int Age { get; }
public AgeOutOfRangeError(int age) => Age = age;
public string Message => $"Age out of range: {Age}";
}
// Result monad
public interface IResult<T, E>
{
bool IsOk { get; }
bool IsError { get; }
IResult<U, E> Bind<U>(Func<T, IResult<U, E>> f);
IResult<U, E> Map<U>(Func<T, U> f);
}
public static class Result<T, E>
{
public static IResult<T, E> Ok(T value) => new OkResult<T, E>(value);
public static IResult<T, E> Error(E error) => new ErrorResult<T, E>(error);
}
public class OkResult<T, E> : IResult<T, E>
{
public T Value { get; }
public OkResult(T value) => Value = value;
public bool IsOk => true;
public bool IsError => false;
public IResult<U, E> Bind<U>(Func<T, IResult<U, E>> f) => f(Value);
public IResult<U, E> Map<U>(Func<T, U> f) => Result<U, E>.Ok(f(Value));
}
public class ErrorResult<T, E> : IResult<T, E>
{
public E Error { get; }
public ErrorResult(E error) => Error = error;
public bool IsOk => false;
public bool IsError => true;
public IResult<U, E> Bind<U>(Func<T, IResult<U, E>> f) =>
new ErrorResult<U, E>(Error);
public IResult<U, E> Map<U>(Func<T, U> f) =>
new ErrorResult<U, E>(Error);
}
// Reader monad with Result
public class ReaderResult<R, T, E>
{
private readonly Func<R, IResult<T, E>> computation;
public ReaderResult(Func<R, IResult<T, E>> computation)
{
this.computation = computation;
}
public static ReaderResult<R, T, E> Pure(T value) =>
new ReaderResult<R, T, E>(_ => Result<T, E>.Ok(value));
public static ReaderResult<R, T, E> Error(E error) =>
new ReaderResult<R, T, E>(_ => Result<T, E>.Error(error));
public ReaderResult<R, U, E> Bind<U>(Func<T, ReaderResult<R, U, E>> f) =>
new ReaderResult<R, U, E>(config =>
{
var result = computation(config);
return result.IsOk
? f(((OkResult<T, E>)result).Value).Run(config)
: Result<U, E>.Error(((ErrorResult<T, E>)result).Error);
});
public ReaderResult<R, U, E> Map<U>(Func<T, U> f) =>
new ReaderResult<R, U, E>(config => computation(config).Map(f));
public IResult<T, E> Run(R config) => computation(config);
// LINQ support
public ReaderResult<R, U, E> SelectMany<U>(Func<T, ReaderResult<R, U, E>> f) => Bind(f);
public ReaderResult<R, V, E> SelectMany<U, V>(
Func<T, ReaderResult<R, U, E>> f,
Func<T, U, V> projection) =>
Bind(t => f(t).Map(u => projection(t, u)));
}
// Person result type
public class Person
{
public string Name { get; set; } = "";
public int Age { get; set; }
public string AgeGroup { get; set; } = "";
public List<string> TransformationHistory { get; set; } = new();
public override string ToString() =>
$"Person {{ Name = \"{Name}\", Age = {Age}, AgeGroup = \"{AgeGroup}\", " +
$"History = [{string.Join(", ", TransformationHistory.Select(h => $"\"{h}\""))}] }}";
}
public class CSVTransformer
{
// Parse age with monadic error handling
static ReaderResult<Config, int, ITransformationError> ParseAge(string ageStr) =>
new ReaderResult<Config, int, ITransformationError>(config =>
{
if (!int.TryParse(ageStr, out var age))
return Result<int, ITransformationError>.Error(new InvalidAgeError(ageStr));
if (config.RequireValidAge && (age < 0 || age > 150))
return Result<int, ITransformationError>.Error(new AgeOutOfRangeError(age));
return Result<int, ITransformationError>.Ok(age);
});
// Categorize age based on configuration
static ReaderResult<Config, string, ITransformationError> CategorizeAge(int age) =>
new ReaderResult<Config, string, ITransformationError>(config =>
{
var ageGroup = age > config.AgeThreshold ? "old" : "young";
return Result<string, ITransformationError>.Ok(ageGroup);
});
// Localize name based on configuration
static ReaderResult<Config, string, ITransformationError> LocalizeName(string name) =>
new ReaderResult<Config, string, ITransformationError>(config =>
{
var localizedName = config.Locale == "de"
? name.ToUpperInvariant()
: name.ToLowerInvariant();
return Result<string, ITransformationError>.Ok(localizedName);
});
// Monadic transformation pipeline using LINQ syntax
public static ReaderResult<Config, Person, ITransformationError> TransformRow(Dictionary<string, string> row)
{
if (!row.ContainsKey("name") || string.IsNullOrEmpty(row["name"]))
return ReaderResult<Config, Person, ITransformationError>.Error(new MissingFieldError("name"));
if (!row.ContainsKey("age") || string.IsNullOrEmpty(row["age"]))
return ReaderResult<Config, Person, ITransformationError>.Error(new MissingFieldError("age"));
var name = row["name"];
var ageStr = row["age"];
// Sequential monadic composition using LINQ
return from age in ParseAge(ageStr)
from ageGroup in CategorizeAge(age)
from localizedName in LocalizeName(name)
select new Person
{
Name = localizedName,
Age = age,
AgeGroup = ageGroup,
TransformationHistory = new List<string>
{
$"Parsed age: {age}",
$"Categorized as: {ageGroup}",
$"Localized name: {localizedName}"
}
};
}
// Process entire CSV with error collection
public static IResult<List<Person>, List<ITransformationError>> TransformCSVData(
List<Dictionary<string, string>> csvData,
Config config)
{
var results = new List<Person>();
var errors = new List<ITransformationError>();
foreach (var row in csvData)
{
var result = TransformRow(row).Run(config);
if (result.IsOk)
results.Add(((OkResult<Person, ITransformationError>)result).Value);
else
errors.Add(((ErrorResult<Person, ITransformationError>)result).Error);
}
return errors.Any()
? Result<List<Person>, List<ITransformationError>>.Error(errors)
: Result<List<Person>, List<ITransformationError>>.Ok(results);
}
// Simple CSV parser
public static List<Dictionary<string, string>> ReadCSV(string csv)
{
var lines = csv.Trim().Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
if (lines.Length < 2) return new List<Dictionary<string, string>>();
var headers = lines[0].Split(',').Select(h => h.Trim()).ToArray();
return lines.Skip(1).Select(line =>
{
var values = line.Split(',').Select(v => v.Trim()).ToArray();
return headers.Zip(values, (k, v) => new { k, v })
.ToDictionary(x => x.k, x => x.v);
}).ToList();
}
}
class Program
{
static void Main()
{
var csvData = CSVTransformer.ReadCSV("name,age\nAlice,25\nBob,invalid\nCharlie,35");
var config = new Config
{
AgeThreshold = 30,
Locale = "de",
RequireValidAge = true
};
var result = CSVTransformer.TransformCSVData(csvData, config);
if (result.IsOk)
{
Console.WriteLine("Transformed data:");
foreach (var person in ((OkResult<List<Person>, List<ITransformationError>>)result).Value)
Console.WriteLine(person);
}
else
{
Console.WriteLine("Transformation errors:");
foreach (var error in ((ErrorResult<List<Person>, List<ITransformationError>>)result).Error)
Console.WriteLine(error.Message);
}
}
}
Conclusion #
The true power of monads becomes apparent when we see how they solve real-world problems:
- Error Propagation: Instead of littering code with null checks and error handling, monads like
MaybeandEitherautomatically manage failure paths - Sequential Dependencies: Unlike applicative functors that handle independent operations, monads excel at computations where each step depends on previous results
- Context Preservation: Whether it's state, configuration, or accumulated logs, monads seamlessly thread context through complex computations
- Composability: Monadic operations compose naturally, allowing complex workflows to be built from simple, reusable pieces
Practical Takeaways #
- Start Simple: Begin with
MaybeandEitherto handle null values and errors elegantly - Think Sequentially: When you need computations that depend on previous results, consider monads over applicative functors
- Embrace Composition: Design small, focused monadic operations that compose into larger workflows
- Use Language Features: Leverage your language's monadic support—LINQ in C#, async/await patterns, or custom implementations
- Don't Overuse: Monads are powerful tools, but not every problem needs a monadic solution
Common Monads #
- Maybe/Option: Handles null values
- Either/Result: Handles errors
- List: Non-deterministic computations
- IO: Side effects and input/output
- State: Stateful computations
- Reader: Environment/context passing
- Writer: Logging and accumulation
- Cont: Continuations and control flow
Monads provide a principled way to handle the messy realities of computation—errors, state, dependencies, and side effects—while maintaining the benefits of functional programming: composability, reasoning, and modularity. Whether you're handling user authentication flows, processing data transformations, or managing complex state transitions, monadic patterns provide a structured approach to sequential computations.
The next time you find yourself writing nested conditionals, chaining nullable operations, or manually managing error states, remember: there's probably a monad for that.
Source code #
Reference implementation (opens in a new tab)