Skip to content
This repository has been archived by the owner on Apr 2, 2023. It is now read-only.

Commit

Permalink
feat: add Either
Browse files Browse the repository at this point in the history
Refactor Maybe on top of Either
  • Loading branch information
tdreyno committed May 2, 2020
1 parent 3ca9e2c commit 1ba23f5
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 58 deletions.
60 changes: 60 additions & 0 deletions src/monads/Either.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { pipe } from "../core/index"

export interface Left<A> {
readonly type: "Left"
readonly value: A
}

export interface Right<B> {
readonly type: "Right"
readonly value: B
}

export type Either<A, B> = Left<A> | Right<B>

export const Left = <A, B = unknown>(value: A): Either<A, B> => ({
type: "Left",
value,
})

export const Right = <B, A = unknown>(value: B): Either<A, B> => ({
type: "Right",
value,
})

export const cata = <A, B, R>(handlers: {
Left: (v: A) => R
Right: (v: B) => R
}) => (either: Either<A, B>): R => {
switch (either.type) {
case "Left":
return handlers.Left(either.value)

case "Right":
return handlers.Right(either.value)
}
}

export const fold = <A, B, U>(LeftFn: (v: A) => U, RightFn: (v: B) => U) =>
cata({ Left: LeftFn, Right: RightFn })

// Bimap
export const bimap = <A, B, C, D>(LeftFn: (v: A) => B, RightFn: (v: C) => D) =>
fold<A, C, Either<B, D>>(
pipe<A, B, Either<B, D>>(LeftFn, Left),
pipe<C, D, Either<B, D>>(RightFn, Right),
)

// Chain
export const chain = <A, B, B2>(fn: (a: B) => Either<A, B2>) =>
fold<A, B, Either<A, B2>>(Left, fn)

// Functor
export const map = <A, B, B2>(fn: (a: B) => B2) =>
chain<A, B, B2>(pipe<B, B2, Either<A, B2>>(fn, Right))

export const isLeft = <A, B>(either: Either<A, B>): either is Left<A> =>
either.type === "Left"

export const isRight = <A, B>(either: Either<A, B>): either is Right<B> =>
either.type === "Right"
70 changes: 31 additions & 39 deletions src/monads/Maybe.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { identity, pipe } from "../core/index"

export interface Just<T> {
readonly type: "Just"
readonly value: T
}

export interface Nothing<T = unknown> {
readonly type: "Nothing"
}

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

export const Just = <T>(value: T): Maybe<T> => ({ type: "Just", value })
export const Nothing = <T>(): Maybe<T> => ({ type: "Nothing" })
import {
Right,
Left,
cata as cata_,
fold as fold_,
chain as chain_,
map as map_,
isLeft,
isRight,
} from "./Either"

type Just<T> = Right<T>
type Nothing<T = unknown> = Left<null>
export type Maybe<T> = Nothing<T> | Just<T>

export const Just = <T>(value: T): Maybe<T> => Right(value)
export const Nothing = <T>(): Maybe<T> => Left(null)

// Monoid
export const empty = Nothing
Expand All @@ -21,40 +24,29 @@ export const empty = Nothing
export const of = Just

export const fromNullable = <T>(valueOrNull: T | null | undefined) =>
valueOrNull ? Just<T>(valueOrNull) : Nothing()

export const cata = <T, U>(handlers: {
Nothing: () => U
Just: (v: T) => U
}) => (maybe: Maybe<T>): U => {
switch (maybe.type) {
case "Just":
return handlers.Just(maybe.value)

case "Nothing":
return handlers.Nothing()
}
}
valueOrNull ? Just<T>(valueOrNull) : Nothing<T>()

export const fold = <T, U>(justFn: (v: T) => U, nothingFn: () => U) =>
cata({ Just: justFn, Nothing: nothingFn })
export const cata = <T, U>(handlers: { Nothing: () => U; Just: (v: T) => U }) =>
cata_({
Left: handlers.Nothing,
Right: handlers.Just,
}) as (maybe: Maybe<T>) => U

// Bimap
export const bimap = <T, U>(justFn: (v: T) => U, nothingFn: () => U) =>
pipe(fold(justFn, nothingFn), Just)
export const fold = <T, U>(nothingFn: () => U, justFn: (v: T) => U) =>
fold_<null, T, U>(nothingFn, justFn) as (maybe: Maybe<T>) => U

// Chain
export const chain = <T, U>(fn: (a: T) => Maybe<U>) =>
fold<T, Maybe<U>>(fn, Nothing)
chain_(fn) as (maybe: Maybe<T>) => Maybe<U>

// Functor
export const map = <T, U>(fn: (a: T) => U) => chain<T, U>(pipe(fn, Just))
export const map = <T, U>(fn: (a: T) => U) =>
map_(fn) as (maybe: Maybe<T>) => Maybe<U>

// Error handling
export const orElse = <T>(fn: () => T) => bimap<T, T>(identity, fn)
export const orElse = <T>(fn: () => T) => pipe(fold<T, T>(fn, identity), Just)

export const isJust = <T>(maybe: Maybe<T>): maybe is Just<T> =>
maybe.type === "Just"
export const isJust = <T>(maybe: Maybe<T>): maybe is Just<T> => isRight(maybe)

export const isNothing = <T>(maybe: Maybe<T>): maybe is Nothing<T> =>
maybe.type === "Nothing"
isLeft(maybe)
83 changes: 83 additions & 0 deletions src/monads/__tests__/Either.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {
Right,
Left,
cata,
fold,
bimap,
map,
chain,
isRight,
isLeft,
} from "../Either"

describe("Either", () => {
test("Right", () => {
const L = jest.fn()
const R = jest.fn()

fold(L, R)(Right(5))

expect(R).toBeCalledWith(5)
expect(L).not.toHaveBeenCalled()
})

test("Left", () => {
const L = jest.fn()
const R = jest.fn()

fold(L, R)(Left(5))

expect(R).not.toHaveBeenCalled()
expect(L).toHaveBeenCalledWith(5)
})

test("cata(Right)", () => {
const L = jest.fn()
const R = jest.fn()

cata({ Left: L, Right: R })(Right(5))

expect(R).toBeCalledWith(5)
expect(L).not.toHaveBeenCalled()
})

test("cata(Left)", () => {
const L = jest.fn()
const R = jest.fn()

cata({ Left: L, Right: R })(Left(5))

expect(R).not.toHaveBeenCalled()
expect(L).toHaveBeenCalledWith(5)
})

test("bimap(Right)", () => {
const L = jest.fn()

expect(bimap(L, (v: number) => v * 2)(Right(5))).toEqual(Right(10))

expect(L).not.toHaveBeenCalled()
})

test("bimap(Left)", () => {
const R = jest.fn()

expect(bimap(() => 10, R)(Left(5))).toEqual(Left(10))

expect(R).not.toHaveBeenCalled()
})

test("map(Right)", () =>
expect(map((v: number) => v * 2)(Right(5))).toEqual(Right(10)))
test("map(Left)", () => expect(map(jest.fn())(Left(5))).toEqual(Left(5)))

test("chain(Right)", () =>
expect(chain((v: number) => Right(v * 2))(Right(5))).toEqual(Right(10)))
test("chain(Left)", () => expect(chain(jest.fn())(Left(5))).toEqual(Left(5)))

test("isRight(Right)", () => expect(isRight(Right(5))).toBe(true))
test("isRight(Left)", () => expect(isRight(Left(5))).toBe(false))

test("isLeft(Right)", () => expect(isLeft(Right(5))).toBe(false))
test("isLeft(Left)", () => expect(isLeft(Left(5))).toBe(true))
})
21 changes: 2 additions & 19 deletions src/monads/__tests__/Maybe.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
empty,
of,
fromNullable,
bimap,
map,
chain,
orElse,
Expand All @@ -19,7 +18,7 @@ describe("Maybe", () => {
const J = jest.fn()
const N = jest.fn()

fold(J, N)(Just(5))
fold(N, J)(Just(5))

expect(J).toBeCalledWith(5)
expect(N).not.toHaveBeenCalled()
Expand All @@ -29,7 +28,7 @@ describe("Maybe", () => {
const J = jest.fn()
const N = jest.fn()

fold(J, N)(Nothing())
fold(N, J)(Nothing())

expect(J).not.toHaveBeenCalled()
expect(N).toHaveBeenCalled()
Expand Down Expand Up @@ -62,22 +61,6 @@ describe("Maybe", () => {
expect(N).toHaveBeenCalled()
})

test("bimap(Just)", () => {
const N = jest.fn()

expect(bimap((v: number) => v * 2, N)(Just(5))).toEqual(Just(10))

expect(N).not.toHaveBeenCalled()
})

test("bimap(Nothing)", () => {
const J = jest.fn()

expect(bimap(J, () => 10)(Nothing())).toEqual(Just(10))

expect(J).not.toHaveBeenCalled()
})

test("map(Just)", () =>
expect(map((v: number) => v * 2)(Just(5))).toEqual(Just(10)))
test("map(Nothing)", () =>
Expand Down

0 comments on commit 1ba23f5

Please sign in to comment.