1import * as F from "effect/Function"
2import assert from "node:assert/strict"
3import { describe, it } from "vitest"
4
5describe("Function", () => {
6 describe("dual", () => {
7 it("arity 2 - data-first", () => {
8 const sum = F.dual<
9 (that: number) => (self: number) => number,
10 (self: number, that: number) => number
11 >(2, (self: number, that: number) => self + that)
12 assert.strictEqual(sum(2, 3), 5)
13 })
14
15 it("arity 2 - data-last", () => {
16 const sum = F.dual<
17 (that: number) => (self: number) => number,
18 (self: number, that: number) => number
19 >(2, (self: number, that: number) => self + that)
20 assert.strictEqual(F.pipe(2, sum(3)), 5)
21 })
22
23 it("arity 3 - data-first", () => {
24 const add3 = F.dual<
25 (b: number, c: number) => (a: number) => number,
26 (a: number, b: number, c: number) => number
27 >(3, (a: number, b: number, c: number) => a + b + c)
28 assert.strictEqual(add3(1, 2, 3), 6)
29 })
30
31 it("arity 3 - data-last", () => {
32 const add3 = F.dual<
33 (b: number, c: number) => (a: number) => number,
34 (a: number, b: number, c: number) => number
35 >(3, (a: number, b: number, c: number) => a + b + c)
36 assert.strictEqual(F.pipe(1, add3(2, 3)), 6)
37 })
38
39 it("arity 4 (default branch) - data-first", () => {
40 const add4 = F.dual(4, (a: number, b: number, c: number, d: number) => a + b + c + d)
41 assert.strictEqual(add4(1, 2, 3, 4), 10)
42 })
43
44 it("arity 4 (default branch) - data-last", () => {
45 const add4 = F.dual(4, (a: number, b: number, c: number, d: number) => a + b + c + d)
46 assert.strictEqual(F.pipe(1, add4(2, 3, 4)), 10)
47 })
48
49 it("throws RangeError for arity 0", () => {
50 assert.throws(() => F.dual(0 as any, () => {}), RangeError)
51 })
52
53 it("throws RangeError for arity 1", () => {
54 assert.throws(() => F.dual(1 as any, (_a: any) => {}), RangeError)
55 })
56
57 it("predicate-based - data-first", () => {
58 const sum = F.dual<
59 (that: number) => (self: number) => number,
60 (self: number, that: number) => number
61 >(
62 (args) => args.length === 2,
63 (self: number, that: number) => self + that
64 )
65 assert.strictEqual(sum(2, 3), 5)
66 })
67
68 it("predicate-based - data-last", () => {
69 const sum = F.dual<
70 (that: number) => (self: number) => number,
71 (self: number, that: number) => number
72 >(
73 (args) => args.length === 2,
74 (self: number, that: number) => self + that
75 )
76 assert.strictEqual(F.pipe(2, sum(3)), 5)
77 })
78 })
79
80 describe("apply", () => {
81 it("applies a value to a function", () => {
82 const len = (s: string) => s.length
83 assert.strictEqual(F.apply("hello")(len), 5)
84 })
85
86 it("works with pipe", () => {
87 const double = (n: number) => n * 2
88 assert.strictEqual(F.pipe(double, F.apply(3)), 6)
89 })
90 })
91
92 describe("identity", () => {
93 it("returns the same value for primitives", () => {
94 assert.strictEqual(F.identity(5), 5)
95 assert.strictEqual(F.identity("hello"), "hello")
96 assert.strictEqual(F.identity(true), true)
97 assert.strictEqual(F.identity(null), null)
98 assert.strictEqual(F.identity(undefined), undefined)
99 })
100
101 it("returns the same reference for objects", () => {
102 const obj = { a: 1 }
103 assert.strictEqual(F.identity(obj), obj)
104 })
105 })
106
107 describe("satisfies", () => {
108 it("returns value unchanged", () => {
109 assert.strictEqual(F.satisfies<number>()(5), 5)
110 })
111 })
112
113 describe("cast", () => {
114 it("returns value unchanged at runtime", () => {
115 assert.strictEqual(F.cast(42), 42)
116 })
117 })
118
119 describe("constant", () => {
120 it("returns the same value every time", () => {
121 const c = F.constant(42)
122 assert.strictEqual(c(), 42)
123 assert.strictEqual(c(), 42)
124 })
125
126 it("returns reference-equal objects", () => {
127 const obj = { a: 1 }
128 const c = F.constant(obj)
129 assert.strictEqual(c(), obj)
130 assert.strictEqual(c() === c(), true)
131 })
132 })
133
134 describe("constTrue", () => {
135 it("returns true", () => {
136 assert.strictEqual(F.constTrue(), true)
137 })
138 })
139
140 describe("constFalse", () => {
141 it("returns false", () => {
142 assert.strictEqual(F.constFalse(), false)
143 })
144 })
145
146 describe("constNull", () => {
147 it("returns null", () => {
148 assert.strictEqual(F.constNull(), null)
149 })
150 })
151
152 describe("constUndefined", () => {
153 it("returns undefined", () => {
154 assert.strictEqual(F.constUndefined(), undefined)
155 })
156 })
157
158 describe("constVoid", () => {
159 it("returns undefined", () => {
160 assert.strictEqual(F.constVoid(), undefined)
161 })
162 })
163
164 describe("flip", () => {
165 it("reverses the order of curried arguments", () => {
166 const f = (a: number) => (b: string) => a - b.length
167 assert.strictEqual(F.flip(f)("aaa")(2), -1)
168 })
169 })
170
171 describe("compose", () => {
172 const increment = (n: number) => n + 1
173 const double = (n: number) => n * 2
174
175 it("data-first: composes left-to-right", () => {
176 assert.strictEqual(F.compose(increment, double)(2), 6)
177 })
178
179 it("data-last: composes with pipe", () => {
180 assert.strictEqual(F.pipe(increment, F.compose(double))(2), 6)
181 })
182 })
183
184 describe("absurd", () => {
185 it("throws an error when called", () => {
186 assert.throws(() => F.absurd(undefined as never), {
187 message: "Called `absurd` function which should be uncallable"
188 })
189 })
190 })
191
192 describe("tupled", () => {
193 it("converts a multi-arg function to accept a tuple", () => {
194 const add = (a: number, b: number) => a + b
195 const addTupled = F.tupled(add)
196 assert.strictEqual(addTupled([1, 2]), 3)
197 })
198 })
199
200 describe("untupled", () => {
201 it("converts a tuple-accepting function to multi-arg", () => {
202 const first = (tuple: [number, string]) => tuple[0]
203 const firstUntupled = F.untupled(first)
204 assert.strictEqual(firstUntupled(1, "a"), 1)
205 })
206 })
207
208 describe("pipe", () => {
209 it("returns value with no functions", () => {
210 assert.strictEqual(F.pipe(5), 5)
211 })
212
213 it("applies one function", () => {
214 assert.strictEqual(F.pipe(5, (n) => n + 1), 6)
215 })
216
217 it("applies two functions", () => {
218 assert.strictEqual(F.pipe(5, (n) => n + 1, (n) => n * 2), 12)
219 })
220
221 it("applies a long chain", () => {
222 assert.strictEqual(
223 F.pipe(
224 1,
225 (n) => n + 1,
226 (n) => n + 1,
227 (n) => n + 1,
228 (n) => n + 1,
229 (n) => n + 1,
230 (n) => n + 1,
231 (n) => n + 1,
232 (n) => n + 1,
233 (n) => n + 1
234 ),
235 10
236 )
237 })
238
239 it("transforms types through the chain", () => {
240 const result = F.pipe(
241 5,
242 (n) => n.toString(),
243 (s) => s.length
244 )
245 assert.strictEqual(result, 1)
246 })
247 })
248
249 describe("flow", () => {
250 it("single function returns it unchanged", () => {
251 const f = (n: number) => n + 1
252 assert.strictEqual(F.flow(f)(1), 2)
253 })
254
255 it("composes two functions", () => {
256 const len = (s: string) => s.length
257 const double = (n: number) => n * 2
258 assert.strictEqual(F.flow(len, double)("aaa"), 6)
259 })
260
261 it("composes three functions", () => {
262 assert.strictEqual(
263 F.flow(
264 (s: string) => s.length,
265 (n) => n * 2,
266 (n) => n + 1
267 )("ab"),
268 5
269 )
270 })
271
272 it("composes up to nine functions", () => {
273 const result = F.flow(
274 (n: number) => n + 1,
275 (n) => n + 1,
276 (n) => n + 1,
277 (n) => n + 1,
278 (n) => n + 1,
279 (n) => n + 1,
280 (n) => n + 1,
281 (n) => n + 1,
282 (n) => n + 1
283 )(0)
284 assert.strictEqual(result, 9)
285 })
286
287 it("first function can accept multiple arguments", () => {
288 const f = F.flow(
289 (a: number, b: number) => a + b,
290 (n) => n * 2
291 )
292 assert.strictEqual(f(1, 2), 6)
293 })
294 })
295
296 describe("hole", () => {
297 it("throws when called", () => {
298 assert.throws(() => F.hole<string>())
299 })
300 })
301
302 describe("SK", () => {
303 it("returns the second argument", () => {
304 assert.strictEqual(F.SK(0, "hello"), "hello")
305 assert.strictEqual(F.SK("a", 42), 42)
306 })
307 })
308
309 describe("memoize", () => {
310 it("caches results for the same object reference", () => {
311 let callCount = 0
312 const f = F.memoize((obj: { n: number }) => {
313 callCount++
314 return obj.n * 2
315 })
316 const key = { n: 5 }
317 assert.strictEqual(f(key), 10)
318 assert.strictEqual(f(key), 10)
319 assert.strictEqual(callCount, 1)
320 })
321
322 it("recomputes for different object references", () => {
323 let callCount = 0
324 const f = F.memoize((obj: { n: number }) => {
325 callCount++
326 return obj.n * 2
327 })
328 assert.strictEqual(f({ n: 5 }), 10)
329 assert.strictEqual(f({ n: 5 }), 10)
330 assert.strictEqual(callCount, 2)
331 })
332 })
333
334 describe("memoizeIdempotent", () => {
335 it("caches the output as a fixed point", () => {
336 let callCount = 0
337 const input = { id: "input" }
338 const output = { id: "output" }
339 const f = F.memoizeIdempotent((obj: { id: string }) => {
340 callCount++
341 return obj === input ? output : obj
342 })
343
344 assert.strictEqual(f(input), output)
345 assert.strictEqual(f(output), output)
346 assert.strictEqual(callCount, 1)
347 })
348
349 it("caches an input that is already a fixed point", () => {
350 let callCount = 0
351 const f = F.memoizeIdempotent((obj: object) => {
352 callCount++
353 return obj
354 })
355 const input = {}
356
357 assert.strictEqual(f(input), input)
358 assert.strictEqual(f(input), input)
359 assert.strictEqual(callCount, 1)
360 })
361 })
362})