-
Notifications
You must be signed in to change notification settings - Fork 0
/
Maybe.spec.js
360 lines (319 loc) · 13.1 KB
/
Maybe.spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
// @ts-check
'use strict'
describe('Maybe', () => {
const Maybe = require('crocks/maybe')
const { dbl, inc } = require('../utils')
/**
* The Maybe type is a great way to safely handle values that may be
* null or undefined and is one of the most basic ADTs that has practical application
* all on its own
*/
describe('construction', () => {
/**
* A Maybe instance will either have a value, wrapped in a Just container
* or have no value and be represented by a Nothing
*/
it('Creates a Just using the Maybe.of method', () => {
const result = Maybe.of(3)
expect(result.inspect()).toEqual('Just 3')
})
// Using `new Maybe` also works
it('Creates a Just with new Maybe', () => {
const result = new Maybe(3)
expect(result.inspect()).toEqual('Just 3')
})
it('Allows us to create a Just directly', () => {
const result = Maybe.Just(3)
expect(result.inspect()).toEqual('Just 3')
})
it('Creates a Nothing with the zero method', () => {
const result = Maybe.zero()
expect(result.inspect()).toEqual('Nothing')
})
it('Allows us to create a Nothing directly', () => {
const result = Maybe.Nothing()
expect(result.inspect()).toEqual('Nothing')
})
/**
* You might think null or undefined would automatically be converted to a Nothing
* but this is not the case. The constructor doesn't come with any opinions.
* That's a good thing :)
*/
it('Will create a Just null', () => {
const justNull = Maybe.of(null)
expect(justNull.inspect()).toEqual('Just null')
})
it('Will create a Just undefined', () => {
const justNull = Maybe.of(undefined)
expect(justNull.inspect()).toEqual('Just undefined')
})
})
/**
* To get back a Just or Nothing we can create a function and make the decision
* ourselves, returning either a Just with the desired value or a Nothing
*/
describe('Maybe from a custom function', () => {
// We can pull the Just and Nothing constructors out by destructuring Maybe
const { Just, Nothing } = Maybe
const getMaybe = value => (value ? Just(value) : Nothing())
it('Returns a Nothing for null', () => {
const maybeNull = getMaybe(null)
expect(maybeNull.inspect()).toEqual('Nothing')
})
it('Returns a Nothing for undefined', () => {
const maybeUndefined = getMaybe(undefined)
expect(maybeUndefined.inspect()).toEqual('Nothing')
})
it('Returns a Just for a non-zero number', () => {
const maybeFour = getMaybe(4) // Just 4
expect(maybeFour.inspect()).toEqual('Just 4')
})
it('Returns a Nothing for 0', () => {
const maybeZero = getMaybe(0)
expect(maybeZero.inspect()).toEqual('Nothing')
})
it('Returns a Nothing for an empty string', () => {
const maybeEmptyString = getMaybe('')
expect(maybeEmptyString.inspect()).toEqual('Nothing')
})
it('Returns a Just for a non-empty string', () => {
const maybeName = getMaybe('Bob')
expect(maybeName.inspect()).toEqual('Just "Bob"')
})
/**
* You get the idea... we can build functions that return Just or Nothing
* based on whatever criteria we want.
*/
})
/**
* We can accomplish the same thing we did with our custom function above
* with the `safe` utility function provided by the crocks library.
* `safe` takes a predicate function (a function that returns a bool) and a value
* and returns a Just or Nothing based on evaluating that predicate against the value
*/
describe('Maybe using the `safe` utility function', () => {
const safe = require('crocks/Maybe/safe')
it('Returns a Nothing when the predicate evaluates to false', () => {
const theMaybe = safe(Boolean, undefined)
expect(theMaybe.inspect()).toEqual('Nothing')
})
it('Returns a Just with the value when the predicate evaluates to true', () => {
const theMaybe = safe(Boolean, 'Something')
expect(theMaybe.inspect()).toEqual('Just "Something"')
})
/**
* And of course, `safe` is curried, so we can create a new function by passing it just
* our predicate and then using that resulting function on multiple values
*/
describe('curried safe', () => {
const betweenOneAndTen = n => n >= 1 && n <= 10
const maybeOneToTen = safe(betweenOneAndTen)
it('Should be a Just', () => {
const result = maybeOneToTen(5)
expect(result.inspect()).toEqual('Just 5')
})
it('Should be a Nothing', () => {
const result = maybeOneToTen(12)
expect(result.inspect()).toEqual('Nothing')
})
/**
* So as we can see here, it's pretty straightforward to get a Nothing
* based on any value if our logic requires it.
*/
})
})
/**
* Now that we can make a maybe, let's see what we can do with them...
* When getting a Maybe from a function, we won't know ahead of time
* if it's a Just or a Nothing, so we need code that can handle either
* scenario, and this is the entire point of the Maybe type
*/
describe('Methods', () => {
describe('.map', () => {
/**
* map will unwrap our Maybe, apply the passed function to a value
* and return the result as a Maybe. That is, in the case where we have
* a value in the form of a Just.
*/
const { Just, Nothing } = Maybe
it('Will apply a function to a value in a Maybe & return a Maybe', () => {
const maybeNumber = Just(3)
const incDblMapped = maybeNumber.map(inc).map(dbl)
expect(incDblMapped.inspect()).toEqual('Just 8')
})
/**
* And what if we have a Nothing?
* Well, we just get a Nothing back. No attempt is made to execute our functions...
* when `map` is called on a Nothing, a Nothing is returned and the function is not executed
*/
it('Will skip the function & return a Nothing if map is called on a Nothing', () => {
const maybeNumber = Nothing()
const incDblMapped = maybeNumber.map(inc).map(dbl)
expect(incDblMapped.inspect()).toEqual('Nothing')
})
})
describe('.chain', () => {
/**
* The `prop` function attempts to pull the value from a key on an object
* If that key is present, you get a Just of the value, otherwise, Nothing
*/
const safe = require('crocks/Maybe/safe')
const prop = require('crocks/Maybe/prop')
// Accept a value, return a maybe based on the predicate
const maybeInRange = safe(val => val >= 25 && val <= 35)
it('Allows you to create nested Maybes', () => {
const inputObject = { name: 'Bob', age: 30 }
const doubledIfInRange = prop('age', inputObject).map(maybeInRange)
/**
* That isn't a typo - the return value here is `Just Just 30`.
* We have a Maybe wrapped in another Maybe
*/
expect(doubledIfInRange.inspect()).toEqual('Just Just 30')
})
it('Can be flattened with chain', () => {
const inputObject = { name: 'Bob', age: 30 }
const doubledIfInRange = prop('age', inputObject).chain(maybeInRange)
expect(doubledIfInRange.inspect()).toEqual('Just 30')
})
})
describe('.option', () => {
/**
* We need to be able to get a value out of a Maybe.
* To do this safely, we'll use `option`. This will accept
* one argument which will be the default value that is
* returned in the case of a Nothing.
*/
it('Returns the value contained in a Just', () => {
const maybeNumber = Maybe.Just(3)
const result = maybeNumber.option(5)
expect(result).toBe(3)
})
it('Returns the default value for a Nothing', () => {
const maybeNumber = Maybe.Nothing()
const result = maybeNumber.option(5)
expect(result).toBe(5)
})
})
describe('.either', () => {
/**
* Another approach for extracting the value from a Maybe
* is with the `either` method. This method accepts two functions,
* a "left" function and a "right" function. The left function will
* be invoked in the case of a Nothing and the right will be
* invoked for a Just. The right function will receive the value
* contained in the Just as an argument. In either case, the
* return value from the invoked function will be the value returned
* from the call to `either`
*/
it('Returns the value result of running the right fn on a Just', () => {
const maybeNumber = Maybe.Just(3)
const result = maybeNumber.either(
() => 5, // not invoked for a Just
n => n * 2 // Applies one final transformation to a Just
)
expect(result).toBe(6)
})
it('Returns the value result of running the right fn on a Just', () => {
const maybeNumber = Maybe.Nothing()
const result = maybeNumber.either(
() => 5, // returns a default value for a Nothing
n => n * 2 // Doesn't get invoked for a Nothing
)
expect(result).toBe(5)
})
})
describe('.alt', () => {
/**
* The Maybe keeps our operations safe by skipping operations that might otherwise
* throw an exception with a null or undefined value
* That's great, what if we end up with a Nothing
* and rather than lose out on an entire series of operations,
* we'd like to continue our data processing with a default value
* in those cases where we end up with a Nothing. The `alt` method
* is one way to handle this situation
*/
it('Provides a default Just for a Nothing', () => {
const theNothing = Maybe.Nothing()
const result = theNothing.alt(Maybe.Just(5))
expect(result.inspect()).toEqual('Just 5')
})
it('Returns the original when called on a Just', () => {
const theJust = Maybe.Just(3)
const result = theJust.alt(Maybe.Just(5))
expect(result.inspect()).toEqual('Just 3')
})
})
})
describe('Maybe.coalesce', () => {
/**
* The `alt` method is one way to "recover" in a situation where
* We end up with a Nothing but we'd like a default Just so we can
* continue processing via `map`s, `chain`s and the like.
* The `coalesce` method gives us another way. This method is
* like `either` in the fact that it takes a "left" and "right"
* function as arguments. It runs the "left" for a Nothing and the
* "right" is invoked with the value when there is a Just. The result
* is a Just that contains the resulting value from whichever function
* ends up being invoked.
*/
it('Will put a value in Just for a Nothing', () => {
const theNothing = Maybe.Nothing()
const result = theNothing.coalesce(
() => 'default value', // we get the default back as a Just
str => str.toLowerCase()
)
expect(result.inspect()).toBe('Just "default value"')
})
it('Will transform the Just', () => {
const theJust = Maybe.Just('ALL CAPPED')
const result = theJust.coalesce(
() => 'default value',
str => str.toLowerCase() // transform is run on our value
)
expect(result.inspect()).toBe('Just "all capped"')
})
it('The identity function can be used to pass through the Just', () => {
const identity = x => x
const theNothing = Maybe.Just('ALL CAPPED')
const result = theNothing.coalesce(() => 'default value', identity)
expect(result.inspect()).toBe('Just "ALL CAPPED"')
})
/**
* The point here, is that we can continue processing after handling
* a potential Nothing case and landing back in a Just state
*/
it('We can continue processing from a Just', () => {
const theJust = Maybe.Just('ALL CAPPED')
const result = theJust
.coalesce(() => 'default value', str => str.toLowerCase())
.map(str => str.split(' '))
expect(result.inspect()).toBe('Just [ "all", "capped" ]')
})
it('We can continue processing from a Nothing', () => {
const theNothing = Maybe.Nothing()
const result = theNothing
.coalesce(() => 'default value', str => str.toLowerCase())
.map(str => str.split(' '))
expect(result.inspect()).toBe('Just [ "default", "value" ]')
})
})
describe('Ramda traverse', () => {
const isNumber = require('crocks/predicates/isNumber')
const { dbl } = require('../utils')
const { traverse, zip } = require('ramda')
it('Creates an array of Maybes without traverse', () => {
const safeDbl = n => isNumber(n) ? Maybe.Just(dbl(n)) : Maybe.Nothing()
const data = [1, 2, 3]
const result = data.map(safeDbl)
const zipped = zip(data, result)
zipped.forEach(([n, m]) => {
expect(m.inspect()).toEqual(`Just ${n * 2}`)
})
})
it('Creates a Maybe of the transformed array', () => {
const safeDbl = n => isNumber(n) ? Maybe.Just(dbl(n)) : Maybe.Nothing()
const result = traverse(Maybe.of, safeDbl, [1, 2, 3])
expect(result.inspect()).toEqual('Just [ 2, 4, 6 ]')
})
})
})