Last active
August 5, 2021 13:32
-
-
Save sylvaindesve/21e2a861aba84afc8676b4c93b74ddb6 to your computer and use it in GitHub Desktop.
Simple Maybe, Either and State implementations in TypeScript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Monad } from "./Monad"; | |
interface EitherType<L, R> extends Monad<R> { | |
isLeft(): boolean; | |
isRight(): boolean; | |
} | |
class Left<L, R> implements EitherType<L, R> { | |
constructor(public readonly left: L) {} | |
public isLeft(): this is Left<L, R> { | |
return true; | |
} | |
public isRight(): this is Right<L, R> { | |
return false; | |
} | |
public map<U>(_fn: (a: R) => U): Either<L, U> { | |
return left(this.left); | |
} | |
public ap<U>(_fn: Either<L, (a: R) => U>): Either<L, U> { | |
return left(this.left); | |
} | |
public chain<U>(fn: (a: R) => Either<L, U>): Either<L, U> { | |
return left(this.left); | |
} | |
public equals(other: Either<L, R>): boolean { | |
return other.isLeft() && other.left === this.left; | |
} | |
} | |
export function left<L, R>(left: L): Left<L, R> { | |
return new Left(left); | |
} | |
class Right<L, R> implements EitherType<L, R> { | |
constructor(public readonly right: R) {} | |
public isLeft(): this is Left<L, R> { | |
return false; | |
} | |
public isRight(): this is Right<L, R> { | |
return true; | |
} | |
public map<U>(fn: (a: R) => U): Either<L, U> { | |
return right(fn(this.right)); | |
} | |
public ap<U>(fn: Either<L, (a: R) => U>): Either<L, U> { | |
if (fn.isRight()) { | |
return this.map(fn.right); | |
} else { | |
return left(fn.left); | |
} | |
} | |
public chain<U>(fn: (a: R) => Either<L, U>): Either<L, U> { | |
return fn(this.right); | |
} | |
public equals(other: Either<L, R>): boolean { | |
return other.isRight() && other.right === this.right; | |
} | |
} | |
export function right<L, R>(right: R): Right<L, R> { | |
return new Right(right); | |
} | |
export type Either<A, B> = Left<A, B> | Right<A, B>; | |
export function liftEither<L, R1, R2>( | |
fn: (t: R1) => Either<L, R2> | |
): (t: Either<L, R1>) => Either<L, R2> { | |
return (t: Either<L, R1>) => { | |
if (t.isRight()) { | |
return fn(t.right); | |
} else { | |
return left(t.left); | |
} | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Just, liftMaybe, Maybe, Nothing } from "./Maybe"; | |
function maybeFind<T>(elems: T[], predicate: (elem: T) => boolean): Maybe<T> { | |
const found = elems.find(predicate); | |
return found ? just(found) : nothing(); | |
} | |
interface Person { | |
name: string; | |
age?: number; | |
} | |
const people: Person[] = [ | |
{ name: "John", age: 32 }, | |
{ name: "Jane", age: 27 }, | |
{ name: "Robert" }, | |
]; | |
const john = maybeFind(people, (person) => person.name === "John"); | |
const britney = maybeFind(people, (person) => person.name === "Britney"); | |
console.log("john", john); // Just { value: { name: 'John', age: 32 } } | |
console.log("britney", britney); // Nothing {} | |
console.log(john.ap(just((p: Person) => p.name.toUpperCase()))); // Just { value: 'JOHN' } | |
console.log(john.ap(nothing<(p: Person) => string>())); // Nothing {} | |
function getAge(person: Person): Maybe<number> { | |
return person.age ? just(person.age) : nothing(); | |
} | |
const robert = maybeFind(people, (person) => person.name === "Robert"); | |
const ageOfJohn = john.chain(getAge); | |
const ageOfBritney = britney.chain(getAge); | |
const ageOfRobert = robert.chain(getAge); | |
console.log("ageOfJohn", ageOfJohn); // Just { value: 32 } | |
console.log("ageOfBritney", ageOfBritney); // Nothing {} | |
console.log("ageOfRobert", ageOfRobert); // Nothing {} | |
const liftedGetAge = liftMaybe(getAge); | |
console.log("liftedGetAge(john)", liftedGetAge(john)); // Just { value: 32 } | |
console.log("liftedGetAge(britney)", liftedGetAge(britney)); // Nothing {} | |
console.log("liftedGetAge(robert)", liftedGetAge(robert)); // Nothing {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Monad } from "./Monad"; | |
interface MaybeType<T> extends Monad<T> { | |
isNothing(): boolean; | |
isJust(): boolean; | |
} | |
class Nothing<T> implements MaybeType<T> { | |
public isNothing(): this is Nothing<T> { | |
return true; | |
} | |
public isJust(): this is Just<T> { | |
return false; | |
} | |
public map<U>(_fn: (a: T) => U): Maybe<U> { | |
return nothing(); | |
} | |
public ap<U>(_fn: Maybe<(a: T) => U>): Maybe<U> { | |
return nothing(); | |
} | |
public chain<U>(_fn: (a: T) => Maybe<U>): Maybe<U> { | |
return nothing(); | |
} | |
public equals(other: Maybe<T>): boolean { | |
return !other.isJust(); | |
} | |
} | |
class Just<T> implements MaybeType<T> { | |
constructor(public readonly value: T) {} | |
public isNothing(): this is Nothing<T> { | |
return false; | |
} | |
public isJust(): this is Just<T> { | |
return true; | |
} | |
public map<U>(fn: (a: T) => U): Maybe<U> { | |
return just(fn(this.value)); | |
} | |
public ap<U>(fn: Maybe<(a: T) => U>): Maybe<U> { | |
if (fn.isJust()) { | |
return this.map(fn.value); | |
} else { | |
return nothing(); | |
} | |
} | |
public chain<U>(fn: (a: T) => Maybe<U>): Maybe<U> { | |
return fn(this.value); | |
} | |
public equals(other: Maybe<T>): boolean { | |
return other.isJust() && other.value === this.value; | |
} | |
} | |
export function nothing<T>() { | |
return new Nothing<T>(); | |
} | |
export function just<T>(value: T): Just<T> { | |
return new Just(value); | |
} | |
export type Maybe<T> = Just<T> | Nothing<T>; | |
export function liftMaybe<A, B>( | |
fn: (t: A) => Maybe<B> | |
): (t: Maybe<A>) => Maybe<B> { | |
return (t: Maybe<A>) => { | |
if (t.isJust()) { | |
return fn(t.value); | |
} else { | |
return nothing(); | |
} | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export interface Functor<T> { | |
map<U>(fn: (t: T) => U): Functor<U> | |
} | |
export interface Apply<T> { | |
ap<U>(fn: Apply<(t: T) => U>): Apply<U>; | |
} | |
export interface Chain<T> { | |
chain<U>(fn: (t: T) => Chain<U>): Chain<U>; | |
} | |
export interface Monad<T> extends Functor<T>, Apply<T>, Chain<T> { | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Monad } from "./Monad"; | |
interface StatefulResult<S, R> { | |
state: S; | |
result: R; | |
} | |
class State<S, R> implements Monad<R> { | |
constructor(public readonly runState: (s: S) => StatefulResult<S, R>) {} | |
public map<U>(fn: (r: R) => U): State<S, U> { | |
return new State((s) => ({ | |
result: fn(this.runState(s).result), | |
state: s, | |
})); | |
} | |
public ap<U>(fn: State<S, (r: R) => U>): State<S, U> { | |
return new State((s) => { | |
const { result: fnResult, state: fnState } = fn.runState(s); | |
const { result, state } = this.runState(fnState); | |
return { result: fnResult(result), state }; | |
}); | |
} | |
public chain<U>(fn: (r: R) => State<S, U>): State<S, U> { | |
return new State((s) => { | |
const { result, state } = this.runState(s); | |
return fn(result).runState(state); | |
}); | |
} | |
public then<U>(computation: State<S, U>): State<S, U> { | |
return new State((s) => { | |
const { state } = this.runState(s); | |
return computation.runState(state); | |
}); | |
} | |
} | |
export function state<S, R>( | |
runState: (s: S) => StatefulResult<S, R> | |
): State<S, R> { | |
return new State(runState); | |
} | |
export function sequenceState_<S, A>( | |
computations: State<S, A>[] | |
): State<S, A> { | |
return computations.reduce( | |
(memo: State<S, A>, computation: State<S, A>) => { | |
return memo.then(computation); | |
} | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Either, Left, liftEither, Right } from "./Either"; | |
function divide(a: number, b: number): Either<string, number> { | |
if (b === 0) return left("cannot divide by zero"); | |
return right(a / b); | |
} | |
console.log("divide(4, 2)", divide(4, 2)); // Right { right: 2 } | |
console.log("divide(4, 0)", divide(4, 0)); // Left { left: 'cannot divide by zero' } | |
function doubleIfEven(a: number): Either<string, number> { | |
if (a % 2 === 0) return right(2 * a); | |
return left("cannot double odd number"); | |
} | |
console.log("divide(6, 3).bind(doubleIfEven)", divide(6, 3).chain(doubleIfEven)); // Right { right: 4 } | |
console.log("divide(6, 3).bind(doubleIfEven)", divide(6, 2).chain(doubleIfEven)); // Left { left: 'cannot double odd number' } | |
console.log("divide(6, 3).bind(doubleIfEven)", divide(6, 0).chain(doubleIfEven)); // Left { left: 'cannot divide by zero' } | |
const liftedDoubleIfEven = liftEither(doubleIfEven); | |
console.log( | |
"liftedDoubleIfEven(divide(6, 3))", | |
liftedDoubleIfEven(divide(6, 3)) | |
); // Right { right: 4 } | |
console.log( | |
"liftedDoubleIfEven(divide(6, 3))", | |
liftedDoubleIfEven(divide(6, 2)) | |
); // Left { left: 'cannot double odd number' } | |
console.log( | |
"liftedDoubleIfEven(divide(6, 3))", | |
liftedDoubleIfEven(divide(6, 0)) | |
); // Left { left: 'cannot divide by zero' } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { State, sequenceState_ } from "./State"; | |
interface AppState { | |
numberOfUsersLoggedIn: number; | |
lastUserLoggedIn: string; | |
logs: string[]; | |
} | |
const initialState: AppState = { | |
numberOfUsersLoggedIn: 0, | |
lastUserLoggedIn: "", | |
logs: [], | |
}; | |
const userDisconnected = state((appState: AppState) => ({ | |
result: null, | |
state: { | |
...appState, | |
numberOfUsersLoggedIn: appState.numberOfUsersLoggedIn - 1, | |
logs: [...appState.logs, "disconnect"], | |
}, | |
})); | |
const userConnected = (name: string) => { | |
return state((appState: AppState) => ({ | |
result: null, | |
state: { | |
...appState, | |
lastUserLoggedIn: name, | |
numberOfUsersLoggedIn: appState.numberOfUsersLoggedIn + 1, | |
logs: [...appState.logs, "connect " + name], | |
}, | |
})); | |
}; | |
console.log(userConnected("John").runState(initialState)); | |
/* | |
{ | |
result: null, | |
state: { | |
numberOfUsersLoggedIn: 1, | |
lastUserLoggedIn: 'John', | |
logs: [ 'connect John' ] | |
} | |
} | |
*/ | |
console.log( | |
userConnected("John").then(userDisconnected).runState(initialState) | |
); | |
/* | |
{ | |
result: null, | |
state: { | |
numberOfUsersLoggedIn: 0, | |
lastUserLoggedIn: 'John', | |
logs: [ 'connect John', 'disconnect' ] | |
} | |
} | |
*/ | |
console.log( | |
sequenceState_([ | |
userConnected("John"), | |
userConnected("Jane"), | |
userConnected("Robert"), | |
userDisconnected, | |
userConnected("John"), | |
userDisconnected, | |
]).runState(initialState) | |
); | |
/* | |
{ | |
result: null, | |
state: { | |
numberOfUsersLoggedIn: 2, | |
lastUserLoggedIn: 'John', | |
logs: [ | |
'connect John', | |
'connect Jane', | |
'connect Robert', | |
'disconnect', | |
'connect John', | |
'disconnect' | |
] | |
} | |
} | |
*/ | |
const getLastUserConnected = state((appState: AppState) => ({ | |
result: appState.lastUserLoggedIn, | |
state: appState, | |
})); | |
// Reconnect last user | |
console.log( | |
getLastUserConnected.chain(userConnected).runState({ | |
numberOfUsersLoggedIn: 30, | |
lastUserLoggedIn: "John", | |
logs: [], | |
}) | |
); | |
/* | |
{ | |
result: null, | |
state: { | |
numberOfUsersLoggedIn: 31, | |
lastUserLoggedIn: 'John', | |
logs: [ 'connect John' ] | |
} | |
} | |
*/ | |
const searchLogs = state((appState: AppState) => { | |
return { | |
result: (search: string) => appState.logs.filter((l) => l.includes(search)), | |
state: appState, | |
}; | |
}); | |
const someState: AppState = { | |
numberOfUsersLoggedIn: 2, | |
lastUserLoggedIn: "John", | |
logs: [ | |
"connect John", | |
"connect Jane", | |
"connect Robert", | |
"disconnect", | |
"connect John", | |
"disconnect", | |
], | |
}; | |
console.log(getLastUserConnected.ap(searchLogs).runState(someState)); | |
/* | |
{ | |
result: [ 'connect John', 'connect John' ], | |
state: { | |
numberOfUsersLoggedIn: 2, | |
lastUserLoggedIn: 'John', | |
logs: [ | |
'connect John', | |
'connect Jane', | |
'connect Robert', | |
'disconnect', | |
'connect John', | |
'disconnect' | |
] | |
} | |
} | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment