-
Notifications
You must be signed in to change notification settings - Fork 2
/
optionsutils.nim
577 lines (554 loc) · 24.1 KB
/
optionsutils.nim
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
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
## This module implements conveniences for dealing with the ``Option`` type in
## Nim. It is based on
## `superfuncs maybe library<https://github.com/superfunc/maybe>`_ and
## `Toccatas novel boolean approach<www.toccata.io/2017/10/No-Booleans.html>`_
## but also implements features found elsewhere.
##
## The goal of this library is to make options in Nim easier and safer to work
## with by creating good patterns for option handling.
##
##
## Usage
## =====
##
## Let's start with the example from the ``options`` module:
##
## .. code-block:: nim
## import options
##
## proc find(haystack: string, needle: char): Option[int] =
## for i, c in haystack:
## if c == needle:
## return some(i)
## return none(int) # This line is actually optional,
## # because the default is empty
##
## .. code-block:: nim
## let found = "abc".find('c')
## assert found.isSome and found.get() == 2
##
## This is probably a familiar pattern, we get an option value, check if it is
## a "some" value, and then extract the actual value. But this is verbose and
## error prone. What if we refactor the code and drop the isSome check, now
## we're in a scenario where ``found.get()`` will throw an exception if we're
## not careful. This module offers a couple of alternatives:
##
## .. code-block:: nim
## withSome found:
## some value: echo value
## none: discard
##
## found?.echo
##
## echo either(found, 0)
##
## The first way, using ``withSome`` offers a safe unpacking pattern. You pass
## it an option, or a list of options, and give it branches to evaluate when
## either all of the options are some, or any of them is a none. The benefit of
## this pattern is that the options values are unpacked into variables
## automatically in the applicable branch. This means that you can't mess up a
## refactoring and move something from the some branch into the none branch.
## And as long as care is taken to stay away from ``options.get`` and
## ``options.unsafeGet`` this will ensure you can't have exception cases.
## ``withSome`` can also be used as a pragma to check that all ``Option[T]``
## arguments to a procedure are some and unpack them automatically.
##
## The second option is the existential operator, or optional chaining
## operator. This operator can be put where regular dot-chaining would apply
## and will only continue execution if the left-hand side is a some. In this
## example ``echo`` will only be called when ``found`` is a some, and won't
## return anything. However this also works when the right hand side might
## return something, in this case it will be wrapped in an ``Option[T]`` that
## will be a none if the left-hand side is a none.
##
## And last but not least, a simple ``either`` template. This takes an option
## and a default value, but where the regular ``options.get`` with an
## ``otherwise`` argument the default value may be a procedure that returns a
## value. This procedure will only be called if the value is a none, making
## sure that no side-effects from the procedure will happen unless it's
## necessary.
##
## This module also includes the convenient ``optCmp`` template allowing you to
## easily compare the values of two options in an option aware manner.
## So instead of having to wrap your options in one of the patterns above you
## can alse use ``optCmp`` to compare options directly:
##
## .. code-block:: nim
## let compared = some(5).optCmp(`<`, some(10))
##
## Note however that this does not return a boolean, it returns the first value
## of the comparisson. So the above code will return a some option with the
## value 5. This means you can use them to filter values for example. And of
## course if either part of the comparisson is a none, then the result will be
## a none as well.
##
## Besides this we also have ``optAnd`` and ``optOr``, these don't work on the
## value of the option, but rather on the has-ity of the option. So ``optOr``
## will return the first some option, or a none option. And ``optAnd`` will
## return the first none option, or the last option. This can be used to
## replace boolean expressions.
##
## .. code-block:: Nim
## let x = "green"
## # This will print out "5"
## echo either(optAnd(optCmp(x, `==`, "green"), 5), 3)
## # This will print out "3"
## echo either(optAnd(optCmp("blue", `==`, "green"), 5), 3)
##
## In the first example ``optAnd`` runs it's first expression, ``optCmp`` which
## returns an option with the value "green", since it has a value ``optAnd``
## runs the second expression ``5`` which is automatically converted to
## ``some(5)``. Since both of these have a value ``optAnd`` returns the last one
## ``some(5)``, the ``either`` procedure is just an alias for ``get`` with a
## default value, since it's first argument has a value it returns that value.
##
## In the second example ``optAnd`` runs it's first expression, ``optCmp`` which
## return an option without a value since the comparisson fails. ``optAnd`` then
## returns an option without a value, and the ``either`` procedure uses the
## default value of 3.
##
## This example is the same as a ``if x == "green": 5 else: 3`` but where x
## might not be set at all.
##
## And last but not least, in case you have a library that doesn't use options
## there are wrapper procedures that wrap exceptions and error codes in option
## returns. This is to work well with the logic operations in this module.
##
## .. code-block:: nim
## let optParseInt = wrapCall: parseInt(x: string): int
## echo optParseInt("10") # Prints "some(10)"
## echo optParseInt("bob") # Prints "None[int]"
## echo either(optParseInt("bob"), 10) # Prints 10, like a default value
## withSome optOr(optParseInt("bob"), 10):
## some value:
## echo 10 # Prints 10, like a default value, but in a safe access pattern
## none:
## echo "No value"
import options, macros
type ExistentialOption[T] = distinct Option[T]
converter toBool*(option: ExistentialOption[bool]): bool =
Option[bool](option).isSome and Option[bool](option).unsafeGet
converter toOption*[T](option: ExistentialOption[T]): Option[T] =
Option[T](option)
proc toExistentialOption*[T](option: Option[T]): ExistentialOption[T] =
ExistentialOption[T](option)
macro `?.`*(option: untyped, statements: untyped): untyped =
## Existential operator. Works like regular dot-chaining, but if
## the left had side is a ``none`` then the right hand side is not evaluated.
## In the case that ``statements`` return something the return type of this
## will be ``ExistentialOption[T]`` where ``T`` is the returned type of
## ``statements`` or if statements return ``Option[T]`` it will be ``T``. If
## nothing is returned from ``statements`` this returns nothing. The
## ``ExistentialOption[T]`` auto-converts to an ``Option[T]`` and the only
## difference between the two is that a ``ExistentialOption[bool]`` will also
## auto-convert to a ``bool`` to allow it to be used in if statements.
##
## .. code-block:: nim
## echo some("Hello")?.find('l') ## Prints out Some(2)
## some("Hello")?.find('l').echo # Prints out 2
## none(string)?.find('l').echo # Doesn't print out anything
## echo none(string)?.find('l') # Prints out None[int] (return type of find)
## # These also work in things like ifs as long as operator precedence is
## # controlled properly:
## if some("Hello")?.find('l').`==` 2:
## echo "This prints"
## proc equalsTwo(x: int): bool = x == 2
## if some("Hello")?.find('l').equalsTwo:
## echo "This also prints"
## if none(string)?.find('l').`==` 2:
## echo "This doesn't"
let opt = genSym(nskLet)
var
injected = statements
firstBarren = statements
if firstBarren.kind in {nnkCall, nnkDotExpr, nnkCommand}:
# This edits the tree that injected points to
while true:
if firstBarren[0].kind notin {nnkCall, nnkDotExpr, nnkCommand}:
firstBarren[0] = nnkDotExpr.newTree(
newCall(bindSym("unsafeGet"), opt), firstBarren[0])
break
firstBarren = firstBarren[0]
else:
injected = nnkDotExpr.newTree(
newCall(bindSym("unsafeGet"), opt), firstBarren)
result = quote do:
(proc (): auto =
let `opt` = `option`
if `opt`.isSome:
when compiles(`injected`) and not compiles(some(`injected`)):
`injected`
else:
return toExistentialOption(toOpt(`injected`))
)()
macro withSome*(options: untyped, body: untyped): untyped =
## Macro to require a set of options to have a value. This macro takes one or
## more statements that returns an option, and two cases for how to handle
## the cases that all the options have a value or that at least one of them
## doesn't. The easiest example looks something like this:
##
## .. code-block:: nim
## withSome "abc".find('b'):
## some pos: echo "Found 'b' at position: ", pos
## none: echo "Couldn't find b"
##
## In order to minimize the nesting of these withSome blocks you can pass a
## list of statements that return an option to require and a list of
## identifiers to the ``some`` case. When doing this the statements will be
## executed one by one, terminating before all statements are evaluated if one
## doesn't return a ``some`` option:
##
## .. code-block:: nim
## withSome ["abc".find('o'), "def".find('f')]:
## some [firstPos, secondPos]:
## echo "Found 'o' at position: ", firstPos, " and 'f' at position ",
## secondPos
## none: echo "Couldn't find either 'o' or 'f'"
##
## This will search for an "o" in the string "abc" which will return a
## ``none`` option and so we will stop, not search for "f" and run
## the ``none`` case. If there are any of the values we don't care about, but
## we still require them to exist we can shadow the identifier. All of these
## would be valid (this is just an example, it is not allowed to have more
## than one ``some`` case):
##
## .. code-block:: nim
## withSome [oneThing, anotherThing]:
## some [firstPos, secondPos]:
## some [_, secondPos]:
## some _:
## withSome [oneThing]:
## some pos:
## some _:
##
## A withSome block can also be used to return values:
##
## .. code-block:: nim
## let x = withSome(["abc".find('b'), "def".find('f')]):
## some [firstPos, secondPos]: firstPos + secondPos
## none: -1
## echo x # Prints out "3" (1 + 2)
var
noneCase: NimNode = nil
someCase: NimNode = nil
idents: NimNode = nil
for optionCase in body:
case optionCase.kind:
of nnkCall:
if $optionCase[0] != "none":
if $optionCase[0] != "some":
error "Only \"none\" and \"some\" are allowed as case labels",
optionCase[0]
else:
error "Only \"none\" is allowed to not have arguments", optionCase[0]
elif noneCase != nil:
error "Only one \"none\" case is allowed, " &
"previously defined \"none\" case at: " & lineInfo(noneCase),
optionCase[0]
else:
noneCase = optionCase[1]
of nnkCommand:
if $optionCase[0] != "some":
if $optionCase[0] != "none":
error "Only \"none\" and \"some\" are allowed as case labels",
optionCase[0]
else:
error "Only \"some\" is allowed to have arguments", optionCase[0]
elif someCase != nil:
error "Only one \"some\" case is allowed, " &
"previously defined \"some\" case at: " & lineInfo(someCase),
optionCase[0]
else:
if optionCase[1].kind != nnkBracket and optionCase[1].kind != nnkIdent:
error "Must have either a list or a single identifier as arguments",
optionCase[1]
else:
if optionCase[1].kind == nnkBracket:
if options.kind != nnkBracket:
error "When only a single option is passed only a single " &
"identifier must be supplied", optionCase[1]
for i in optionCase[1]:
if i.kind != nnkIdent:
error "List must only contain identifiers", i
elif options.kind == nnkBracket:
if $optionCase[1] != "_":
error "When multiple options are passed all identifiers must be " &
"supplied", optionCase[1]
idents = if optionCase[1].kind == nnkBracket: optionCase[1] else: newStmtList(optionCase[1])
someCase = optionCase[2]
else:
error "Unrecognized structure of cases", optionCase
if noneCase == nil and someCase == nil:
error "Must have either a \"some\" case, a \"none\" case, or both"
var
body = if someCase != nil: someCase else: nnkDiscardStmt.newTree(newNilLit())
none = if noneCase != nil: noneCase else: nnkDiscardStmt.newTree(newNilLit())
let
optionsList = (if options.kind == nnkBracket: options else: newStmtList(options))
ug = bindSym"unsafeGet"
for i in countdown(optionsList.len - 1, 0):
let
option = optionsList[i]
tmpLet = genSym(nskLet)
ident = if idents.len <= i: newLit("_") else: idents[i]
assign = if $ident != "_":
quote do:
let `ident` = `ug`(`tmpLet`)
else:
newStmtList()
body = quote do:
let `tmpLet` = `option`
if `tmpLet`.isSome:
`assign`
`body`
else:
`none`
result = body
# This doesn't work if `body` includes any reference to result..
# It was probably done this way for a reason though
#result = quote do:
# (proc (): auto =
# `body`
# )()
template either*(self, otherwise: untyped): untyped =
## Similar in function to ``get``, but if ``otherwise`` is a procedure it will
## not be evaluated if ``self`` is a ``some``. This means that ``otherwise``
## can have side effects.
let opt = self # In case self is a procedure call returning an option
if opt.isSome: unsafeGet(opt) else: otherwise
macro wrapCall*(statement: untyped): untyped =
## Macro that wraps a procedure which can throw an exception into one that
## returns an option. This version takes a procedure with arguments and a
## return type. It returns a lambda that has the same signature as the
## procedure but returns an Option of the return type. The body executes the
## statement and returns the value if there is no exception, otherwise it
## returns a none option.
##
## .. code-block:: nim
## let optParseInt = wrapCall: parseInt(x: string): int
## echo optParseInt("10") # Prints "some(10)"
## echo optParseInt("bob") # Prints "none(int)"
assert(statement.kind == nnkStmtList)
assert(statement[0].kind == nnkCall)
assert(statement[0].len == 2)
assert(statement[0][0].kind == nnkObjConstr)
assert(statement[0][0].len >= 1)
assert(statement[0][0][0].kind == nnkIdent)
for i in 1 ..< statement[0][0].len:
assert(statement[0][0][i].kind == nnkExprColonExpr)
assert(statement[0][0][i].len == 2)
assert(statement[0][0][i][0].kind == nnkIdent)
assert(statement[0][1].kind == nnkStmtList)
let T = statement[0][1][0]
let
procName = statement[0][0][0]
result = quote do:
(proc (): Option[`T`] =
try:
return some(`procName`())
except:
return none[`T`]()
)
# Add the arguments to the argument list of the proc and the call
for i in 1 ..< statement[0][0].len:
result[0][3].add nnkIdentDefs.newTree(statement[0][0][i][0], statement[0][0][i][1], newEmptyNode())
result[0][6][0][0][0][0][1].add statement[0][0][i][0]
macro wrapException*(statement: untyped): untyped =
## Macro that wraps a procedure which can throw an exception into one that
## returns an option. This version takes a procedure with arguments but no
## return type. It returns a lambda that has the same signature as the
## procedure but returns an ``Option[ref Exception]``. The body executes the
## statement and returns a none option if there is no exception. Otherwise it
## returns a some option with the exception.
##
## .. code-block:: nim
## # This might be a silly example, it's more useful for things that
## # doesn't return anything
## let optParseInt = wrapException: parseInt(x: string)
## withSome optParseInt("bob"):
## some e: echo e.msg # Prints the exception message
## none: echo "Execution succeded"
assert(statement.len == 1)
assert(statement[0].kind == nnkObjConstr)
assert(statement[0].len >= 1)
assert(statement[0][0].kind == nnkIdent)
for i in 1 ..< statement[0].len:
assert(statement[0][i].kind == nnkExprColonExpr)
assert(statement[0][i].len == 2)
assert(statement[0][i][0].kind == nnkIdent)
let
procName = statement[0][0]
result = quote do:
(proc (): Option[ref Exception] =
try:
discard `procName`()
return none(ref Exception)
except:
return some(getCurrentException())
)
# Add the arguments to the argument list of the proc and the call
for i in 1 ..< statement[0].len:
result[0][3].add nnkIdentDefs.newTree(statement[0][i][0], statement[0][i][1], newEmptyNode())
result[0][6][0][0][0][0].add statement[0][i][0]
macro wrapErrorCode*(statement: untyped): untyped =
## Macro that wraps a procedure which returns an error code into one that
## returns an option. This version takes a procedure with arguments but no
## return type. It returns a lambda that has the same signature as the
## procedure but returns an ``Option[int]``. The body executes the
## statement and returns a none option if the error code is 0. Otherwise it
## returns a some option with the error code.
##
## .. code-block:: nim
## # We cheat a bit here and use parseInt to emulate an error code
## let optParseInt = wrapErrorCode: parseInt(x: string)
## withSome optParseInt("10"):
## some e: echo "Got error code: ", e
## none: echo "Execution succeded"
assert(statement.len == 1)
assert(statement[0].kind == nnkObjConstr)
assert(statement[0].len >= 1)
assert(statement[0][0].kind == nnkIdent)
for i in 1 ..< statement[0].len:
assert(statement[0][i].kind == nnkExprColonExpr)
assert(statement[0][i].len == 2)
assert(statement[0][i][0].kind == nnkIdent)
let
procName = statement[0][0]
result = quote do:
(proc (): Option[int] =
let eCode = `procName`()
if eCode == 0:
return none(int)
else:
return some(eCode)
)
# Add the arguments to the argument list of the proc and the call
for i in 1 ..< statement[0].len:
result[0][3].add nnkIdentDefs.newTree(statement[0][i][0], statement[0][i][1], newEmptyNode())
result[0][6][0][0][2].add statement[0][i][0]
proc toOpt*[T](value: Option[T]): Option[T] =
## Procedure with overload to automatically convert something to an option if
## it's not already an option.
value
proc toOpt*[T](value: T): Option[T] =
## Procedure with overload to automatically convert something to an option if
## it's not already an option.
some(value)
macro optAnd*(options: varargs[untyped]): untyped =
## Goes through all options until one of them is not a some. If one of the
## options is not a some it returns a none, otherwise it returns the last
## option. Note that if some of the options are a procedure that returns an
## Option they won't get evaluated if an earlier option is a none. If any of
## the options is not an option but another type they will be converted to an
## option of that type automatically.
var
body = newStmtList()
lastOpt: NimNode
for option in options:
lastOpt = genSym(nskLet)
body.add quote do:
let `lastOpt` = toOpt(`option`)
if not `lastOpt`.isSome: return
body.add quote do:
return `lastOpt`
result = quote do:
(proc (): auto = `body`)()
macro optOr*(options: varargs[untyped]): untyped =
## Goes through the options until one of them is a some. If none of the
## options are a some a none is returned. Note that if some of the options are
## a procedure that returns an Option they won't get evaluated if an earlier
## option is a some. If any of the options is not an option but another type
## they will be converted to an option of that type automatically.
var body = newStmtList()
for option in options:
body.add quote do:
let opt = toOpt(`option`)
if opt.isSome: return opt
result = quote do:
(proc (): auto = `body`)()
template optCmp*(self, cmp, value: untyped): untyped =
## Comparator for options. ``cmp`` must be something that accepts two
## parameters, ``self`` and ``value`` can either be ``Option[T]`` or ``T``.
## Will return ``self`` if it is an ``Option[T]`` or ``self`` converted to
## an ``Option[T]`` if both ``self`` and ``value`` is a some and ``cmp``
## returns true when called with their values.
(proc (): auto =
let
a = toOpt(self)
b = toOpt(value)
if a.isSome and b.isSome:
if `cmp`(unsafeGet(a), unsafeGet(b)):
return a
)()
macro withSome*(procDef: untyped): untyped =
## Pragma that can be applied to procedures which takes ``Option[T]``
## arguments. It will verify that all the options are some and shadow the
## argument names with the internal type of the ``Option[T]`` (as long as the
## argument isn't declared as a ``var``). If one of the arguments isn't
## ``some`` then the procedure will return before it's body, and the return
## value will be left as its default (so an ``Option[T]`` return type would be
## a ``none``).
runnableExamples:
import options
proc addNums(a, b: Option[int], c: var Option[int]): Option[int] {.withSome.} =
assert typeof(a) is int # `a` and `b` are shadowed with the options internal type
assert typeof(b) is int
assert typeof(c) is Option[int] # `c` is not shadowed as it is a `var`
result = some(a + b + c.get())
c = none(int)
# Since the default of an `Option[int]` is a `none`, if the check of `a`
# and `b` fails this returns `none`
doAssert procDef.kind in {nnkProcDef, nnkFuncDef}, "This macro only works on procedure and function definitions."
let
identDefs = procDef[3] #All parameter names
stmtList = procDef.body #body
for def in identDefs:
let
opt = def.findChild(it.kind in {nnkBracketExpr, nnkVarTy})
bracket = if opt == nil or opt.kind == nnkBracketExpr: opt else: opt[0]
if bracket != nil and $bracket[0] == "Option":
var foundNonIdent = false #Used to remove redundant constant checks
for varNode in def:
foundNonIdent = varNode.kind != nnkIdent #First N idents are the declared variables in a `a,b,c: T`
if foundNonIdent: break #Hit non ident which means we dont need to iterate any further
stmtList.insert 0, quote do:
if `varNode`.isNone: return
if opt.kind != nnkVarTy:
stmtList.insert 1, quote do:
let `varNode` = `varNode`.unsafeGet
procDef
macro withNone*(procDef: untyped): untyped =
## Same as the ``withSome`` pragma but verifies that all options are ``none``.
## All option arguments are shadowed by an error template so they can't be
## used within the procedure body, this does not apply to arguments declared
## as ``var``.
doAssert procDef.kind in {nnkProcDef, nnkFuncDef}, "This macro only works on procedure and function definitions."
let
identDefs = procDef[3] #All parameter names
stmtList = procDef.body
for def in identDefs:
let
opt = def.findChild(it.kind in {nnkBracketExpr, nnkVarTy})
bracket = if opt == nil or opt.kind == nnkBracketExpr: opt else: opt[0]
#Get all defined variables here so we can check them later
if bracket != nil and $bracket[0] == "Option":
var foundNonIdent = false #Used to remove redundant constant checks
for varNode in def:
foundNonIdent = varNode.kind != nnkIdent #First N idents are the declared variables in a `a,b,c: T`
if foundNonIdent: break #Hit non ident which means we dont need to iterate any further
stmtList.insert 0, quote do:
if `varNode`.isSome: return
if opt.kind != nnkVarTy:
stmtList.insert 1, quote do:
template `varNode`(): untyped = {.error: "Cannot use `" & astToStr(`varNode`) & "` in this `withNone` context".}
procDef
macro then*[T](cond: bool, val: T): untyped =
## When the condition is true the result is ``some(val)``.
## Otherwise the result is ``none(val.type)``
runnableExamples:
assert 100.toOpt == true.then(100)
quote do:
if `cond`:
some(`val`)
else:
none(`val`.type)