Skip to content

Instantly share code, notes, and snippets.

@sylvaindesve
Last active August 5, 2021 13:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sylvaindesve/21e2a861aba84afc8676b4c93b74ddb6 to your computer and use it in GitHub Desktop.
Save sylvaindesve/21e2a861aba84afc8676b4c93b74ddb6 to your computer and use it in GitHub Desktop.
Simple Maybe, Either and State implementations in TypeScript
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);
}
};
}
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 {}
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();
}
};
}
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> {
}
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);
}
);
}
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' }
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