/
optional.js
252 lines (220 loc) · 6 KB
/
optional.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
// An implementation of the Optional type, sometimes known as the Maybe or Option type.
//
// An instance of an option type is an optional value. Either it's `none`, or an
// instance of `Some`:
//
// ```javascript
// import optional from "utility/optional";
//
// const some = optional.some("Bob");
// const none = optional.none;
// ```
//
// A function that returns an optional string isn't that different from a function
// that returns a string or `null`. The advantage over null is that optionals
// provide a number of functions that help with manipulating optional values.
//
// ```javascript
// function greet(user) {
// return "Hello " + user.name().valueOrElse("Anonymous");
// }
// ```
//
// Why bother in a dynamic language? We want to provide explicit objects that represent
// the common case of missing values. In this service, this is used as a signal that you should
// consider a result that may be missing (usually due to referential integrity). These Optionals
// provide several convenience methods to make it easy to explicitly consider if you should
// throw or handle missing values.
//
// For the curious: we implement in way most similar to swift, where you must specify how you
// access with `must` (!) or `maybe` (?). This implementation is mostly lifted from
// https://github.com/mwilliamson/node-options but with some API changes to be more strict with
// access (`must` vs `maybe`) and to add a couple more convenience methods.
//
// --------------------------------------------
// Functions (Ways to create / test Optionals)
// --------------------------------------------
//
// ### optional.some()
//
// * Yields a "some" optional that represents presense of a value. If undefined, an error will
// be thrown.
//
// ### optional.none
//
// * The none optional that represents lack of a value. Note that this is NOT a function but
// an instance of Optional.
//
// ### optional.isOptional(*value*)
//
// * `option.isOption(value)` returns `true` if `value` is `option.none` or `option.some(x)`.
//
// ### option.fromNullable(*value*)
//
// * If `value` is `null` or `undefined`, `option.fromNullable(value)` returns `option.none`.
// * Otherwise, returns `option.some(value)`.
// For instance, `option.fromNullable(5)` returns `option.some(5)`
//
// --------------------------------------------
// Instance Methods
// --------------------------------------------
//
// ### match(*pattern*)
//
// A function that returns the result of the function.
// Throws an error if you don't provide the correct arguments.
//
// ```js
// match({ Some: (value) => { /* ... */ }, None: () => { /* ... */ } });
// ```
//
// * `some(value)` returns `pattern.Some(value)`
// * `none` returns `pattern.None()`
//
// ### maybe()
//
// A loose unwrap function. This does an optional unwrap where you get the internal value.
//
// * `some(value)` returns `value`
// * `none` returns `undefined`.
//
// ### must()
//
// A force unwrap function. Be careful with this method as it indicates you know if the value you
// expect should exist or make noise if not.
//
// * `some(value)` returns `value`
// * `none` throws an Error.
//
// ### isNone() and isSome()
//
// * `some(value).isNone()` returns `false`
// * `some(value).isSome()` returns `true`
// * `none.isNone()` returns `true`
// * `none.isSome()` returns `false`
//
// ### toArray()
//
// * `some(value).toArray()` returns `[some]`
// * `none.toArray()` returns `[]`
//
// ### orElse(*other*)
//
// Ensure that other returns anther Optional or an error will be thrown.
//
// If `other` is a function (`other` returning another option):
//
// * `some(value).orElse(other)` returns `some(value)`
// * `none.orElse(other)` returns `other()`
//
// If `other` is not a function (`other` returning another option):
//
// * `some(value).orElse(other)` returns `some(value)`
// * `none.orElse(other)` returns `other`
//
// ### valueOrElse(*other*)
//
// If `other` is a function:
//
// * `some(value).valueOrElse(other)` returns `value`
// * `none.valueOrElse(other)` returns `other()`
//
// If `other` is not a function:
//
// * `some(value).valueOrElse(other)` returns `value`
// * `none.valueOrElse(other)` returns `other`
/* eslint-disable class-methods-use-this */
import _ from "lodash";
function callable(x) {
return _.isFunction(x) ? x() : x;
}
function validatePattern(pattern) {
if (!pattern || !pattern.Some || !pattern.None) {
throw new Error("`match` requires both pattern cases (`Some` and `None`)");
}
}
class Optional {
tap(callback) {
callback(this.maybe());
return this;
}
}
class Some extends Optional {
constructor(val) {
super();
if (_.isUndefined(val)) {
throw new Error("Created an Optional.Some with `undefined`");
}
this.maybe = () => val;
}
must() {
return this.maybe();
}
match(pattern) {
validatePattern(pattern);
return pattern.Some(this.maybe());
}
isNone() {
return false;
}
isSome() {
return true;
}
orElse = () => {
return this;
}
valueOrElse = () => {
return this.maybe();
}
toArray = () => {
return [this.maybe()];
}
}
class None extends Optional {
maybe() {} // return undefined
must() {
throw new Error("Unwrapped an optional with `must` but it is `none`");
}
match(pattern) {
validatePattern(pattern);
return pattern.None();
}
isNone() {
return true;
}
isSome() {
return false;
}
orElse = (optionalOrFunc) => {
const optional = callable(optionalOrFunc);
if (!(optional instanceof Optional)) {
throw new Error("Optional.orElse must return another optional");
}
return optional;
}
valueOrElse = (valOrFunc) => {
return callable(valOrFunc);
}
toArray() {
return [];
}
}
function some(val) {
return new Some(val);
}
const none = new None();
function isOptional(val) {
return val === none || val instanceof Some;
}
function fromNullable(val) {
if (_.isUndefined(val) || _.isNull(val)) {
return none;
}
return new Some(val);
}
export default {
some,
none,
isOptional,
fromNullable,
};