Skip to content

Commit 2d72cf0

Browse files
committed
Extend options stdlib
1 parent 08642ff commit 2d72cf0

File tree

2 files changed

+190
-3
lines changed

2 files changed

+190
-3
lines changed

lib/pure/options.nim

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ when defined(nimHasEffectsOf):
7474
else:
7575
{.pragma: effectsOf.}
7676

77-
import std/typetraits
77+
import std/[typetraits, macros]
7878

7979
when defined(nimPreviewSlimSystem):
8080
import std/assertions
@@ -379,3 +379,129 @@ proc unsafeGet*[T](self: Option[T]): lent T {.inline.}=
379379
## Generally, using the `get proc <#get,Option[T]>`_ is preferred.
380380
assert self.isSome
381381
result = self.val
382+
383+
macro evalOnceAs(expAlias, exp: untyped): untyped =
384+
## Injects `expAlias` in caller scope, evaluating it only once and ensuring no copies are made.
385+
expectKind(expAlias, nnkIdent)
386+
return if exp.kind != nnkSym:
387+
newLetStmt(expAlias, exp)
388+
else:
389+
newProc(name = genSym(nskTemplate, $expAlias), params = [getType(untyped)],
390+
body = exp, procType = nnkTemplateDef)
391+
392+
template withValue*[T](source: Option[T]; varname, ifExists, ifAbsent: untyped) =
393+
## Reads a value from an Option, assigns it to a variable, and calls `ifExists` when it is `some`.
394+
## If the value is `none`, it calls `ifAbsent`.
395+
runnableExamples:
396+
some("abc").withValue(foo):
397+
assert foo == "abc"
398+
do:
399+
assert false
400+
401+
var absentCalled: bool
402+
none(int).withValue(foo):
403+
assert false
404+
do:
405+
absentCalled = true
406+
assert absentCalled
407+
408+
block:
409+
evalOnceAs(local, source)
410+
if local.isSome:
411+
template varname(): auto {.inject, used.} = unsafeGet(local)
412+
ifExists
413+
else:
414+
ifAbsent
415+
416+
template withValue*[T](source: Option[T]; varname, ifExists: untyped) =
417+
## Reads a value from an Option, assigns it to a variable, and calls `ifExists` when it is `some`.
418+
runnableExamples:
419+
some("abc").withValue(foo):
420+
assert foo == "abc"
421+
422+
none(int).withValue(foo):
423+
assert false
424+
425+
source.withValue(varname, ifExists):
426+
discard
427+
428+
template mapIt*[T](value: Option[T], action: untyped): untyped =
429+
## Applies an action to the value of the `Option`, if it has one.
430+
runnableExamples:
431+
assert some(42).mapIt(it * 2).mapIt($it) == some("84")
432+
assert none(int).mapIt(it * 2).mapIt($it) == none(string)
433+
434+
block:
435+
type InnerType = typeof(
436+
block:
437+
var it {.inject, used.}: typeof(value.get())
438+
action
439+
)
440+
441+
var outcome: Option[InnerType]
442+
value.withValue(it):
443+
outcome = some(action)
444+
outcome
445+
446+
template flatMapIt*[T](value: Option[T], action: untyped): untyped =
447+
## Executes an action on the value of the `Option`, where that action can also return an `Option`.
448+
runnableExamples:
449+
assert some(42).flatMapIt(some($it)) == some("42")
450+
assert some(42).flatMapIt(none(string)) == none(string)
451+
assert none(int).flatMapIt(some($it)) == none(string)
452+
assert none(int).flatMapIt(none(string)) == none(string)
453+
454+
block:
455+
type InnerType = typeof(
456+
block:
457+
var it {.inject, used.}: typeof(value.get())
458+
action.get()
459+
)
460+
461+
var outcome: Option[InnerType]
462+
value.withValue(it):
463+
outcome = action
464+
outcome
465+
466+
template filterIt*[T](value: Option[T], action: untyped): Option[T] =
467+
## Tests the value of the `Option` with a predicate, returning a `none` if it fails.
468+
runnableExamples:
469+
assert some(42).filterIt(it > 0) == some(42)
470+
assert none(int).filterIt(it > 0) == none(int)
471+
assert some(-11).filterIt(it > 0) == none(int)
472+
473+
block:
474+
var outcome = value
475+
outcome.withValue(it):
476+
if not action:
477+
outcome = none(T)
478+
do:
479+
outcome = none(T)
480+
outcome
481+
482+
template applyIt*[T](value: Option[T], action: untyped) =
483+
## Executes a code block if the `Option` is `some`, assigning the value to a variable named `it`
484+
runnableExamples:
485+
var value: string
486+
some("foo").applyIt:
487+
value = it
488+
assert value == "foo"
489+
490+
none(string).applyIt:
491+
assert false
492+
493+
value.withValue(it):
494+
action
495+
496+
template `or`*[T](a, b: Option[T]): Option[T] =
497+
## Returns the value of the `Option` if it has one, otherwise returns the other `Option`.
498+
runnableExamples:
499+
assert((some(42) or some(9999)) == some(42))
500+
assert((none(int) or some(9999)) == some(9999))
501+
assert((none(int) or none(int)) == none(int))
502+
block:
503+
evalOnceAs(local, a)
504+
if local.isSome:
505+
local
506+
else:
507+
b

tests/stdlib/toptions.nim

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ discard """
55

66
import std/[json, options]
77

8-
import std/assertions
9-
import std/objectdollar
8+
import std/[assertions, macros, objectdollar]
109

1110

1211
# RefPerson is used to test that overloaded `==` operator is not called by
@@ -19,6 +18,11 @@ proc `==`(a, b: RefPerson): bool =
1918
assert(not a.isNil and not b.isNil)
2019
a.name == b.name
2120

21+
type Uncopiable = distinct int
22+
23+
proc `=copy`(dest: var Uncopiable; source: Uncopiable) {.error.}
24+
25+
proc `==`(a, b: Uncopiable): bool {.borrow.}
2226

2327
template disableJsVm(body) =
2428
# something doesn't work in JS VM
@@ -28,6 +32,17 @@ template disableJsVm(body) =
2832
else:
2933
body
3034

35+
macro buildOnce(value: untyped): untyped =
36+
## Creates a proc that can be called only once
37+
let name = genSym(nskProc)
38+
return quote:
39+
proc `name`(): auto =
40+
var called {.global.} = false
41+
doAssert(not called, "Expression should only be executed once")
42+
called = true
43+
return `value`
44+
`name`
45+
3146
proc main() =
3247
type
3348
Foo = ref object
@@ -197,6 +212,52 @@ proc main() =
197212
doAssert x.isNone
198213
doAssert $x == "none(cstring)"
199214

215+
# withValue should only evaluate the expression once
216+
block:
217+
let someValue = buildOnce(some(Uncopiable(42)))
218+
someValue().withValue(value):
219+
doAssert(value.int == 42)
220+
do:
221+
doAssert false
222+
223+
# withValue should only evaluate the expression once
224+
block:
225+
let someValue = buildOnce(some(Uncopiable(42)))
226+
someValue().withValue(value):
227+
doAssert(value.int == 42)
228+
229+
# mapIt should only evalute its expression once
230+
block:
231+
let someValue = buildOnce(some(Uncopiable(42)))
232+
doAssert someValue().mapIt($it.int) == some("42")
233+
234+
# flatMapIt should only evalute its expression once
235+
block:
236+
let someValue = buildOnce(some(Uncopiable(42)))
237+
doAssert someValue().flatMapIt(some($it.int)) == some("42")
238+
239+
# filterIt should only evaluate its expression once
240+
block:
241+
let someValue = buildOnce(some(Uncopiable(42)))
242+
var outcome: int
243+
someValue().applyIt:
244+
outcome = it.int
245+
doAssert outcome == 42
246+
247+
# or should only evaluate its expression once
248+
block:
249+
let a = buildOnce(some(42))
250+
doAssert a().or(some(0)) == some(42)
251+
252+
let b = buildOnce(some(42))
253+
doAssert none(int).or(b()) == some(42)
254+
255+
# or should not copy values
256+
# Disabled on JS because of: internal error: ("genAddr: 2", skTemp)
257+
when not defined(js):
258+
block:
259+
doAssert some(Uncopiable(42)).or(some(Uncopiable(0))) == some(Uncopiable(42))
260+
200261
static: main()
201262
main()
202263

0 commit comments

Comments
 (0)