An educational pure functional programming library in TypeScript
1import { describe, expect, it } from "bun:test"
2import {
3 apOption,
4 apResult,
5 err,
6 liftA2Option,
7 liftA2Result,
8 none,
9 ok,
10 optionInstances,
11 pipe,
12 resultInstances,
13 some,
14} from "../src/index"
15
16describe("Type Classes", () => {
17 describe("apResult", () => {
18 it("applies function in Ok to value in Ok", () => {
19 const rf = ok((x: number) => x * 2)
20 const ra = ok(10)
21 const result = pipe(rf, apResult(ra))
22 expect(result).toEqual(ok(20))
23 })
24
25 it("returns Err when function is Err", () => {
26 const rf = err("no function")
27 const ra = ok(10)
28 const result = pipe(rf, apResult(ra))
29 expect(result).toEqual(err("no function"))
30 })
31
32 it("returns Err when value is Err", () => {
33 const rf = ok((x: number) => x * 2)
34 const ra = err("no value")
35 const result = pipe(rf, apResult(ra))
36 expect(result).toEqual(err("no value"))
37 })
38
39 it("returns first Err when both are Err (short-circuit)", () => {
40 const rf = err("func error")
41 const ra = err("value error")
42 const result = pipe(rf, apResult(ra))
43 expect(result).toEqual(err("func error"))
44 })
45
46 it("works with curried functions", () => {
47 const add = (a: number) => (b: number) => a + b
48 const result = pipe(ok(add), apResult(ok(10)), apResult(ok(5)))
49 expect(result).toEqual(ok(15))
50 })
51 })
52
53 describe("apOption", () => {
54 it("applies function in Some to value in Some", () => {
55 const of_ = some((x: number) => x * 2)
56 const oa = some(10)
57 const result = pipe(of_, apOption(oa))
58 expect(result).toEqual(some(20))
59 })
60
61 it("returns None when function is None", () => {
62 const of_ = none as typeof none & { _tag: "None" }
63 const oa = some(10)
64 const result = pipe(of_, apOption(oa))
65 expect(result._tag).toBe("None")
66 })
67
68 it("returns None when value is None", () => {
69 const of_ = some((x: number) => x * 2)
70 const oa = none
71 const result = pipe(of_, apOption(oa))
72 expect(result._tag).toBe("None")
73 })
74
75 it("works with curried functions", () => {
76 const add = (a: number) => (b: number) => a + b
77 const result = pipe(some(add), apOption(some(10)), apOption(some(5)))
78 expect(result).toEqual(some(15))
79 })
80 })
81
82 describe("liftA2Result", () => {
83 it("lifts binary function to work with Results", () => {
84 const add = (a: number, b: number) => a + b
85 const addResults = liftA2Result(add)
86
87 expect(addResults(ok(1), ok(2))).toEqual(ok(3))
88 })
89
90 it("returns Err when first is Err", () => {
91 const add = (a: number, b: number) => a + b
92 const addResults = liftA2Result(add)
93
94 expect(addResults(err("first"), ok(2))).toEqual(err("first"))
95 })
96
97 it("returns Err when second is Err", () => {
98 const add = (a: number, b: number) => a + b
99 const addResults = liftA2Result(add)
100
101 expect(addResults(ok(1), err("second"))).toEqual(err("second"))
102 })
103
104 it("returns first Err when both are Err", () => {
105 const add = (a: number, b: number) => a + b
106 const addResults = liftA2Result(add)
107
108 expect(addResults(err("first"), err("second"))).toEqual(err("first"))
109 })
110 })
111
112 describe("liftA2Option", () => {
113 it("lifts binary function to work with Options", () => {
114 const add = (a: number, b: number) => a + b
115 const addOptions = liftA2Option(add)
116
117 expect(addOptions(some(1), some(2))).toEqual(some(3))
118 })
119
120 it("returns None when first is None", () => {
121 const add = (a: number, b: number) => a + b
122 const addOptions = liftA2Option(add)
123
124 expect(addOptions(none, some(2))._tag).toBe("None")
125 })
126
127 it("returns None when second is None", () => {
128 const add = (a: number, b: number) => a + b
129 const addOptions = liftA2Option(add)
130
131 expect(addOptions(some(1), none)._tag).toBe("None")
132 })
133 })
134
135 describe("resultInstances", () => {
136 it("has map function", () => {
137 const result = pipe(
138 ok(5),
139 resultInstances.map((x) => x * 2),
140 )
141 expect(result).toEqual(ok(10))
142 })
143
144 it("has of function (alias for ok)", () => {
145 expect(resultInstances.of(42)).toEqual(ok(42))
146 })
147
148 it("has ap function", () => {
149 const result = pipe(
150 ok((x: number) => x + 1),
151 resultInstances.ap(ok(5)),
152 )
153 expect(result).toEqual(ok(6))
154 })
155
156 it("has flatMap function", () => {
157 const result = pipe(
158 ok(5),
159 resultInstances.flatMap((x) => ok(x * 2)),
160 )
161 expect(result).toEqual(ok(10))
162 })
163
164 it("has bimap function", () => {
165 const result = pipe(
166 ok(5),
167 resultInstances.bimap(
168 (x) => x * 2,
169 (e: string) => e.toUpperCase(),
170 ),
171 )
172 expect(result).toEqual(ok(10))
173 })
174 })
175
176 describe("optionInstances", () => {
177 it("has map function", () => {
178 const result = pipe(
179 some(5),
180 optionInstances.map((x) => x * 2),
181 )
182 expect(result).toEqual(some(10))
183 })
184
185 it("has of function (alias for some)", () => {
186 expect(optionInstances.of(42)).toEqual(some(42))
187 })
188
189 it("has ap function", () => {
190 const result = pipe(
191 some((x: number) => x + 1),
192 optionInstances.ap(some(5)),
193 )
194 expect(result).toEqual(some(6))
195 })
196
197 it("has flatMap function", () => {
198 const result = pipe(
199 some(5),
200 optionInstances.flatMap((x) => some(x * 2)),
201 )
202 expect(result).toEqual(some(10))
203 })
204 })
205
206 describe("Functor laws (Result)", () => {
207 it("identity: map(id) === id", () => {
208 const id = <T>(x: T) => x
209 const original = ok(42)
210 expect(pipe(original, resultInstances.map(id))).toEqual(original)
211 })
212
213 it("composition: map(f . g) === map(f) . map(g)", () => {
214 const f = (x: number) => x * 2
215 const g = (x: number) => x + 1
216 const fg = (x: number) => f(g(x))
217
218 const original = ok(5)
219 const left = pipe(original, resultInstances.map(fg))
220 const right = pipe(
221 original,
222 resultInstances.map(g),
223 resultInstances.map(f),
224 )
225
226 expect(left).toEqual(right)
227 })
228 })
229
230 describe("liftA2Option (both-None)", () => {
231 it("returns None when both are None", () => {
232 const add = (a: number, b: number) => a + b
233 const addOptions = liftA2Option(add)
234 expect(addOptions(none, none)._tag).toBe("None")
235 })
236 })
237
238 describe("Monad laws (Result)", () => {
239 it("left identity: flatMap(f)(of(a)) === f(a)", () => {
240 const f = (x: number) => ok(x * 2)
241 const a = 5
242
243 const left = pipe(resultInstances.of(a), resultInstances.flatMap(f))
244 const right = f(a)
245
246 expect(left).toEqual(right)
247 })
248
249 it("right identity: flatMap(of)(m) === m", () => {
250 const m = ok(42)
251
252 const result = pipe(m, resultInstances.flatMap(resultInstances.of))
253
254 expect(result).toEqual(m)
255 })
256 })
257
258 describe("Functor laws (Option)", () => {
259 it("identity: map(id) === id", () => {
260 const id = <T>(x: T) => x
261 const original = some(42)
262 expect(pipe(original, optionInstances.map(id))).toEqual(original)
263 })
264
265 it("identity on None: map(id) === id", () => {
266 const id = <T>(x: T) => x
267 expect(pipe(none, optionInstances.map(id))).toEqual(none)
268 })
269
270 it("composition: map(f . g) === map(f) . map(g)", () => {
271 const f = (x: number) => x * 2
272 const g = (x: number) => x + 1
273 const fg = (x: number) => f(g(x))
274
275 const original = some(5)
276 const left = pipe(original, optionInstances.map(fg))
277 const right = pipe(
278 original,
279 optionInstances.map(g),
280 optionInstances.map(f),
281 )
282
283 expect(left).toEqual(right)
284 })
285 })
286
287 describe("Monad laws (Option)", () => {
288 it("left identity: flatMap(f)(of(a)) === f(a)", () => {
289 const f = (x: number) => some(x * 2)
290 const a = 5
291
292 const left = pipe(optionInstances.of(a), optionInstances.flatMap(f))
293 const right = f(a)
294
295 expect(left).toEqual(right)
296 })
297
298 it("right identity: flatMap(of)(m) === m", () => {
299 const m = some(42)
300
301 const result = pipe(m, optionInstances.flatMap(optionInstances.of))
302
303 expect(result).toEqual(m)
304 })
305
306 it("associativity: flatMap(g)(flatMap(f)(m)) === flatMap(x => flatMap(g)(f(x)))(m)", () => {
307 const f = (x: number) => some(x * 2)
308 const g = (x: number) => some(x + 1)
309
310 const m = some(5)
311 const left = pipe(m, optionInstances.flatMap(f), optionInstances.flatMap(g))
312 const right = pipe(
313 m,
314 optionInstances.flatMap((x) => pipe(f(x), optionInstances.flatMap(g))),
315 )
316
317 expect(left).toEqual(right)
318 })
319 })
320})