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

Commit 1ba23f5

Browse files
committed
feat: add Either
Refactor Maybe on top of Either
1 parent 3ca9e2c commit 1ba23f5

File tree

4 files changed

+176
-58
lines changed

4 files changed

+176
-58
lines changed

src/monads/Either.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { pipe } from "../core/index"
2+
3+
export interface Left<A> {
4+
readonly type: "Left"
5+
readonly value: A
6+
}
7+
8+
export interface Right<B> {
9+
readonly type: "Right"
10+
readonly value: B
11+
}
12+
13+
export type Either<A, B> = Left<A> | Right<B>
14+
15+
export const Left = <A, B = unknown>(value: A): Either<A, B> => ({
16+
type: "Left",
17+
value,
18+
})
19+
20+
export const Right = <B, A = unknown>(value: B): Either<A, B> => ({
21+
type: "Right",
22+
value,
23+
})
24+
25+
export const cata = <A, B, R>(handlers: {
26+
Left: (v: A) => R
27+
Right: (v: B) => R
28+
}) => (either: Either<A, B>): R => {
29+
switch (either.type) {
30+
case "Left":
31+
return handlers.Left(either.value)
32+
33+
case "Right":
34+
return handlers.Right(either.value)
35+
}
36+
}
37+
38+
export const fold = <A, B, U>(LeftFn: (v: A) => U, RightFn: (v: B) => U) =>
39+
cata({ Left: LeftFn, Right: RightFn })
40+
41+
// Bimap
42+
export const bimap = <A, B, C, D>(LeftFn: (v: A) => B, RightFn: (v: C) => D) =>
43+
fold<A, C, Either<B, D>>(
44+
pipe<A, B, Either<B, D>>(LeftFn, Left),
45+
pipe<C, D, Either<B, D>>(RightFn, Right),
46+
)
47+
48+
// Chain
49+
export const chain = <A, B, B2>(fn: (a: B) => Either<A, B2>) =>
50+
fold<A, B, Either<A, B2>>(Left, fn)
51+
52+
// Functor
53+
export const map = <A, B, B2>(fn: (a: B) => B2) =>
54+
chain<A, B, B2>(pipe<B, B2, Either<A, B2>>(fn, Right))
55+
56+
export const isLeft = <A, B>(either: Either<A, B>): either is Left<A> =>
57+
either.type === "Left"
58+
59+
export const isRight = <A, B>(either: Either<A, B>): either is Right<B> =>
60+
either.type === "Right"

src/monads/Maybe.ts

Lines changed: 31 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import { identity, pipe } from "../core/index"
2-
3-
export interface Just<T> {
4-
readonly type: "Just"
5-
readonly value: T
6-
}
7-
8-
export interface Nothing<T = unknown> {
9-
readonly type: "Nothing"
10-
}
11-
12-
export type Maybe<T> = Just<T> | Nothing<T>
13-
14-
export const Just = <T>(value: T): Maybe<T> => ({ type: "Just", value })
15-
export const Nothing = <T>(): Maybe<T> => ({ type: "Nothing" })
2+
import {
3+
Right,
4+
Left,
5+
cata as cata_,
6+
fold as fold_,
7+
chain as chain_,
8+
map as map_,
9+
isLeft,
10+
isRight,
11+
} from "./Either"
12+
13+
type Just<T> = Right<T>
14+
type Nothing<T = unknown> = Left<null>
15+
export type Maybe<T> = Nothing<T> | Just<T>
16+
17+
export const Just = <T>(value: T): Maybe<T> => Right(value)
18+
export const Nothing = <T>(): Maybe<T> => Left(null)
1619

1720
// Monoid
1821
export const empty = Nothing
@@ -21,40 +24,29 @@ export const empty = Nothing
2124
export const of = Just
2225

2326
export const fromNullable = <T>(valueOrNull: T | null | undefined) =>
24-
valueOrNull ? Just<T>(valueOrNull) : Nothing()
25-
26-
export const cata = <T, U>(handlers: {
27-
Nothing: () => U
28-
Just: (v: T) => U
29-
}) => (maybe: Maybe<T>): U => {
30-
switch (maybe.type) {
31-
case "Just":
32-
return handlers.Just(maybe.value)
33-
34-
case "Nothing":
35-
return handlers.Nothing()
36-
}
37-
}
27+
valueOrNull ? Just<T>(valueOrNull) : Nothing<T>()
3828

39-
export const fold = <T, U>(justFn: (v: T) => U, nothingFn: () => U) =>
40-
cata({ Just: justFn, Nothing: nothingFn })
29+
export const cata = <T, U>(handlers: { Nothing: () => U; Just: (v: T) => U }) =>
30+
cata_({
31+
Left: handlers.Nothing,
32+
Right: handlers.Just,
33+
}) as (maybe: Maybe<T>) => U
4134

42-
// Bimap
43-
export const bimap = <T, U>(justFn: (v: T) => U, nothingFn: () => U) =>
44-
pipe(fold(justFn, nothingFn), Just)
35+
export const fold = <T, U>(nothingFn: () => U, justFn: (v: T) => U) =>
36+
fold_<null, T, U>(nothingFn, justFn) as (maybe: Maybe<T>) => U
4537

4638
// Chain
4739
export const chain = <T, U>(fn: (a: T) => Maybe<U>) =>
48-
fold<T, Maybe<U>>(fn, Nothing)
40+
chain_(fn) as (maybe: Maybe<T>) => Maybe<U>
4941

5042
// Functor
51-
export const map = <T, U>(fn: (a: T) => U) => chain<T, U>(pipe(fn, Just))
43+
export const map = <T, U>(fn: (a: T) => U) =>
44+
map_(fn) as (maybe: Maybe<T>) => Maybe<U>
5245

5346
// Error handling
54-
export const orElse = <T>(fn: () => T) => bimap<T, T>(identity, fn)
47+
export const orElse = <T>(fn: () => T) => pipe(fold<T, T>(fn, identity), Just)
5548

56-
export const isJust = <T>(maybe: Maybe<T>): maybe is Just<T> =>
57-
maybe.type === "Just"
49+
export const isJust = <T>(maybe: Maybe<T>): maybe is Just<T> => isRight(maybe)
5850

5951
export const isNothing = <T>(maybe: Maybe<T>): maybe is Nothing<T> =>
60-
maybe.type === "Nothing"
52+
isLeft(maybe)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import {
2+
Right,
3+
Left,
4+
cata,
5+
fold,
6+
bimap,
7+
map,
8+
chain,
9+
isRight,
10+
isLeft,
11+
} from "../Either"
12+
13+
describe("Either", () => {
14+
test("Right", () => {
15+
const L = jest.fn()
16+
const R = jest.fn()
17+
18+
fold(L, R)(Right(5))
19+
20+
expect(R).toBeCalledWith(5)
21+
expect(L).not.toHaveBeenCalled()
22+
})
23+
24+
test("Left", () => {
25+
const L = jest.fn()
26+
const R = jest.fn()
27+
28+
fold(L, R)(Left(5))
29+
30+
expect(R).not.toHaveBeenCalled()
31+
expect(L).toHaveBeenCalledWith(5)
32+
})
33+
34+
test("cata(Right)", () => {
35+
const L = jest.fn()
36+
const R = jest.fn()
37+
38+
cata({ Left: L, Right: R })(Right(5))
39+
40+
expect(R).toBeCalledWith(5)
41+
expect(L).not.toHaveBeenCalled()
42+
})
43+
44+
test("cata(Left)", () => {
45+
const L = jest.fn()
46+
const R = jest.fn()
47+
48+
cata({ Left: L, Right: R })(Left(5))
49+
50+
expect(R).not.toHaveBeenCalled()
51+
expect(L).toHaveBeenCalledWith(5)
52+
})
53+
54+
test("bimap(Right)", () => {
55+
const L = jest.fn()
56+
57+
expect(bimap(L, (v: number) => v * 2)(Right(5))).toEqual(Right(10))
58+
59+
expect(L).not.toHaveBeenCalled()
60+
})
61+
62+
test("bimap(Left)", () => {
63+
const R = jest.fn()
64+
65+
expect(bimap(() => 10, R)(Left(5))).toEqual(Left(10))
66+
67+
expect(R).not.toHaveBeenCalled()
68+
})
69+
70+
test("map(Right)", () =>
71+
expect(map((v: number) => v * 2)(Right(5))).toEqual(Right(10)))
72+
test("map(Left)", () => expect(map(jest.fn())(Left(5))).toEqual(Left(5)))
73+
74+
test("chain(Right)", () =>
75+
expect(chain((v: number) => Right(v * 2))(Right(5))).toEqual(Right(10)))
76+
test("chain(Left)", () => expect(chain(jest.fn())(Left(5))).toEqual(Left(5)))
77+
78+
test("isRight(Right)", () => expect(isRight(Right(5))).toBe(true))
79+
test("isRight(Left)", () => expect(isRight(Left(5))).toBe(false))
80+
81+
test("isLeft(Right)", () => expect(isLeft(Right(5))).toBe(false))
82+
test("isLeft(Left)", () => expect(isLeft(Left(5))).toBe(true))
83+
})

src/monads/__tests__/Maybe.spec.ts

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
empty,
77
of,
88
fromNullable,
9-
bimap,
109
map,
1110
chain,
1211
orElse,
@@ -19,7 +18,7 @@ describe("Maybe", () => {
1918
const J = jest.fn()
2019
const N = jest.fn()
2120

22-
fold(J, N)(Just(5))
21+
fold(N, J)(Just(5))
2322

2423
expect(J).toBeCalledWith(5)
2524
expect(N).not.toHaveBeenCalled()
@@ -29,7 +28,7 @@ describe("Maybe", () => {
2928
const J = jest.fn()
3029
const N = jest.fn()
3130

32-
fold(J, N)(Nothing())
31+
fold(N, J)(Nothing())
3332

3433
expect(J).not.toHaveBeenCalled()
3534
expect(N).toHaveBeenCalled()
@@ -62,22 +61,6 @@ describe("Maybe", () => {
6261
expect(N).toHaveBeenCalled()
6362
})
6463

65-
test("bimap(Just)", () => {
66-
const N = jest.fn()
67-
68-
expect(bimap((v: number) => v * 2, N)(Just(5))).toEqual(Just(10))
69-
70-
expect(N).not.toHaveBeenCalled()
71-
})
72-
73-
test("bimap(Nothing)", () => {
74-
const J = jest.fn()
75-
76-
expect(bimap(J, () => 10)(Nothing())).toEqual(Just(10))
77-
78-
expect(J).not.toHaveBeenCalled()
79-
})
80-
8164
test("map(Just)", () =>
8265
expect(map((v: number) => v * 2)(Just(5))).toEqual(Just(10)))
8366
test("map(Nothing)", () =>

0 commit comments

Comments
 (0)