According to Wikipedia a monad is a design pattern that combines functions and wraps their return types in a wrapped type with additional computation.
In Layman’s terms, you take a type T, and you want to create a pipeline of chained functions performing computation on an argument of type T where each function should take into account the state of T as returned by the previous function call.
Components:
- wrapper type
- wrap function
- run function
One of the representations that I liked the most about how monads work (with credits from monad ):
There are laws that must be respected with monads (like the composition preservation law) but honestly I didn’t really care about checking that out, as long as there is a pattern here which adds value to my code.
My requirements which led to the implementation
- I wanted to access nested properties without checking for nulls at each step
- Each “monady” step would return a different T in Wrapped<T> because we are accessing properties
My implementation :
- is a simple object that contains both the wrapped type , the wrapping function and and the run function.
- has methods not part of the pattern, like ValueOrNull which are not part of the Monad pattern, by it was quite usefull.
- Has a Run (which returns the same T) and RunTransform (which returns the new T)
export class Option { private readonly _value: T | null | undefined; private readonly _hasValue: boolean; public get Value(): T { if (!this._hasValue) { throw `This option does not have a value`; } return this._value; } public get HasValue(): boolean { return this._hasValue; } constructor(value: T | null | undefined) { if (value === null || value === undefined) { this._hasValue = false; this._value = null; } else { this._value = value; this._hasValue = true; } } public static From(value: T | null | undefined): Option { return new Option(value); } public Run(execute: (value: T) => Option): Option { return OptionRun(this, execute); } public RunTransform(execute: (value: T) => Option | TNew): Option { return OptionRunTransform<T, TNew>(this, tr => { let res = execute(tr); if (res instanceof Option) { return res; } else { return Option.From(res); } }); } public ValueOr(defaultValue: T): T { if (this._hasValue) { return this.Value; } else { return defaultValue; } } public ValueOrNull(): T | null{ if (this._hasValue) { return this.Value; } else { return null; } } } export function OptionRun(input: Option, functor: (value: T) => Option): Option { if (input.HasValue) { return functor(input.Value); } return input; } export function OptionRunTransform<T, TNew>(input: Option, functor: (value: T) => Option): Option { if (input.HasValue) { return functor(input.Value); } return Option.From(null); }
Usage
let name = Option.From(activity) .RunTransform(r => r.ActivityTypeNode) .RunTransform(r => r.ActivityType) .RunTransform(r => r.Name) .ValueOr(''); }
instead of
let name = (activity && activity.ActivityTYpeNode && activity.ActivityTypeNode.ActivityType && activity.ActivityTypeNode.ActivityType.Name) ?activity.ActivityTypeNode.ActivityType.Name.Name : ''; }