Skip to main content

Monads: Either or Result

ยท 8 min read
Pere Pages

functional

Both Result and Either types are considered monads in functional programming. They are commonly used for error handling and control flow in languages that support functional programming paradigms, such as Haskell, Scala, and even in TypeScript with certain libraries.

Both Result and Either are monads because they implement the monadic interface, specifically the bind (or flatMap, andThen) function, and satisfy the monad laws (Identity and Associativity).

These types are powerful abstractions for dealing with computations that might fail, providing a way to sequence operations while propagating errors in a clean, functional way.

Naming conventionsโ€‹

They are the same, but have different naming conventions.

Resultโ€‹

tip

Result is more standardized in languages like Rust where the primary use case is error-handling.

In the Result type convention, the first element usually represents the successful value, and the second element represents the error.

The Result type typically represents a computation that may fail. It's often implemented as an algebraic data type with two variants: Ok for successful computations and Err for failed ones.

  • Languages: Commonly found in languages like Rust, and some functional libraries in TypeScript, JavaScript, and C#.
  • Use Case: Specifically designed to represent the outcome of operations that can fail. It often holds a value on success (Ok) and an error on failure (Err).
  • Standardization: In Rust, it's part of the standard library and is heavily integrated into the language's error-handling mechanics.

Eitherโ€‹

tip

Either is more standardized in languages with a strong functional programming history, such as Haskell.

In the Either type, the convention is often the opposite: the first element is for the error, and the second is for the value.

The Either type is more general than Result. It's used to represent a value that can be one of two types: Left or Right. By convention, Left is used for failures and Right is used for successes.

  • Languages: Commonly found in languages like Haskell, Scala, and some functional libraries for JavaScript.
  • Use Case: Designed to hold one of two types. It's more generic than Result and can represent any kind of binary choice, not just success or failure. For example, in Haskell, - Either can be used to hold a Left value that represents an error or a Right value that represents a success.
  • Standardization: In some languages like Haskell, it's part of the standard library.

Monad rulesโ€‹

In functional programming, a Monad is a design pattern used to handle program-wide concerns, such as state or I/O, in a pure functional way. Monads have three primary properties:

Rules of a Monadโ€‹

  1. Left Identity: When a value is put into a monadic context using a constructor (often called return or pure) and then immediately transformed with a function using bind (often >>= in Haskell, flatMap in Scala, or bind in JavaScript), the monad should behave as if you'd just transformed the value itself.

    return a >>= f  โ‰ก  f a
  2. Right Identity: When you have a monadic value and use bind to feed it into return, the result should be the monadic value itself.

    m >>= return  โ‰ก  m
  3. Associativity: When you have a chain of monadic function applications with bind, it should not matter how they're nested.

    (m >>= f) >>= g  โ‰ก  m >>= (\x -> f x >>= g)

TypeScript Exampleโ€‹

Let's create a simplified Maybe Monad in TypeScript to demonstrate these principles. The Maybe Monad is used for computations that may fail and either contain a value (Just(value)) or nothing (Nothing).

Here is how we might define a Maybe Monad in TypeScript:

type Maybe<T> = Just<T> | Nothing;

class Just<T> {
constructor(private value: T) {}

bind<U>(f: (value: T) => Maybe<U>): Maybe<U> {
return f(this.value);
}
}

class Nothing {
bind<U>(_f: (value: any) => Maybe<U>): Maybe<U> {
return this;
}
}

// Helper functions
const just = <T>(value: T): Maybe<T> => new Just(value);
const nothing = new Nothing();

Now let's test the three Monad laws:

  1. Left Identity

    const leftIdentity = (value: number) => just(value).bind(just);
    console.log(leftIdentity(5)); // Output should be Just { value: 5 }
  2. Right Identity

    const rightIdentity = (value: Maybe<number>) => value.bind(just);
    console.log(rightIdentity(just(5))); // Output should be Just { value: 5 }
  3. Associativity

    const f = (x: number) => just(x + 1);
    const g = (x: number) => just(x * 2);

    const lhs = just(5).bind(f).bind(g); // (m >>= f) >>= g
    const rhs = just(5).bind(x => f(x).bind(g)); // m >>= (\x -> f x >>= g)

    console.log(lhs); // Output should be Just { value: 12 }
    console.log(rhs); // Output should be Just { value: 12 }

All the Monad laws hold true for this example, confirming that it's a valid Monad implementation.

Typescript implementation exampleโ€‹

There are many implementations of Result and Either in TypeScript. Here is one example of a Result type that implements the monadic interface:

  • static methods: ok and err to create a Result instance.
  • map and flatMap to transform the value inside the Result.
  • match to handle the two possible cases.
  • getOrElse to get the value or a default value.
type Err<E> = {
tag: 'Err';
value: E;
};

type Ok<T> = {
tag: 'Ok';
value: T;
};

interface Result<T, E> {
readonly isOk: boolean;
readonly isErr: boolean;
map<V>(fn: (value: T) => V): Result<V, E>;
flatMap<V>(fn: (value: T) => Result<V, E>): Result<V, E>;
match<V>(cases: { err: (value: E) => V; ok: (value: T) => V }): V;
getOrElse(defaultValue: T): T;
}

class ResultClass<T, E> implements Result<T, E> {
private constructor(private readonly _value: Err<E> | Ok<T>) {}

static err<T, E>(value: E): Result<T, E> {
return new ResultClass<T, E>({ tag: 'Err', value });
}

static ok<T, E>(value: T): Result<T, E> {
return new ResultClass<T, E>({ tag: 'Ok', value });
}

get isOk(): boolean {
return this._value.tag === 'Ok';
}

get isErr(): boolean {
return this._value.tag === 'Err';
}

map<V>(fn: (value: T) => V): Result<V, E> {
if (this._value.tag === 'Ok') {
return ResultClass.ok(fn(this._value.value));
}
return ResultClass.err(this._value.value);
}

flatMap<V>(fn: (value: T) => Result<V, E>): Result<V, E> {
if (this._value.tag === 'Ok') {
return fn(this._value.value);
}
return ResultClass.err(this._value.value);
}

match<V>(cases: { err: (value: E) => V; ok: (value: T) => V }): V {
switch (this._value.tag) {
case 'Err':
return cases.err(this._value.value);
case 'Ok':
return cases.ok(this._value.value);
}
}

getOrElse(defaultValue: T): T {
if (this._value.tag === 'Ok') {
return this._value.value;
}
return defaultValue;
}
}

export default ResultClass;
export type { Result };

Unit testsโ€‹

import Result from './Result';

describe('Result/Either', () => {
describe('err', () => {
it('should create an Err instance', () => {
const result = Result.err<number, string>('error');
expect(
result.match({
err: value => value,
ok: () => 'unexpected',
})
).toBe('error');
});
});

describe('ok', () => {
it('should create an Ok instance', () => {
const result = Result.ok<number, string>(42);
expect(
result.match({
err: () => NaN,
ok: value => value,
})
).toBe(42);
});
});

describe('map', () => {
it('should map over Ok value', () => {
const result = Result.ok<number, string>(21).map(x => x * 2);
expect(
result.match({
err: () => NaN,
ok: value => value,
})
).toBe(42);
});

it('should not map over Err value', () => {
const result = Result.err<number, string>('error').map(x => x * 2);
expect(
result.match({
err: value => value,
ok: () => 'unexpected',
})
).toBe('error');
});
});

describe('flatMap', () => {
it('should flatMap over Ok value', () => {
const result = Result.ok<number, string>(21).flatMap(x => Result.ok<number, string>(x * 2));
expect(
result.match({
err: () => NaN,
ok: value => value,
})
).toBe(42);
});

it('should not flatMap over Err value', () => {
const result = Result.err<number, string>('error').flatMap(x => Result.ok<number, string>(x * 2));
expect(
result.match({
err: value => value,
ok: () => 'unexpected',
})
).toBe('error');
});
});

describe('match', () => {
it('should execute the err case when Err', () => {
const result = Result.err<number, string>('error');
expect(
result.match({
err: value => value,
ok: () => 'unexpected',
})
).toBe('error');
});

it('should execute the ok case when Ok', () => {
const result = Result.ok<number, string>(42);
expect(
result.match({
err: () => NaN,
ok: value => value,
})
).toBe(42);
});
});

describe('getOrElse', () => {
it('should return the value when it is an Ok', () => {
const okInstance = Result.ok(42);
const result = okInstance.getOrElse(0);
expect(result).toBe(42);
});

it('should return the default value when it is an Err', () => {
const errInstance = Result.err('An error occurred');
const result = errInstance.getOrElse(0);
expect(result).toBe(0);
});
});

describe('isOk', () => {
it('should return true for Ok results', () => {
const result: Result<number, string> = ResultClass.ok(42);
expect(result.isOk).toBe(true);
});

it('should return false for Err results', () => {
const result: Result<number, string> = ResultClass.err("An error occurred");
expect(result.isOk).toBe(false);
});
});

describe('isErr', () => {
it('should return false for Ok results', () => {
const result: Result<number, string> = ResultClass.ok(42);
expect(result.isErr).toBe(false);
});

it('should return true for Err results', () => {
const result: Result<number, string> = ResultClass.err("An error occurred");
expect(result.isErr).toBe(true);
});
});
});