Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
bring the language in line with the standard library (e.g. ``parseOct``).
- The dot style for import paths (e.g ``import path.to.module`` instead of
``import path/to/module``) has been deprecated.
- The ``options`` module has been split in two

#### Breaking changes in the standard library

Expand Down Expand Up @@ -75,6 +76,7 @@
- ``net.sendTo`` no longer returns an int and now raises an ``OSError``.
- `threadpool`'s `await` and derivatives have been renamed to `blockUntil`
to avoid confusions with `await` from the `async` macro.
- ``options`` has been split into ``options`` and ``optionsutils``


#### Breaking changes in the compiler
Expand Down Expand Up @@ -165,6 +167,7 @@
the desired target type (as a concrete type or as a type class)
- The `type` operator now supports checking that the supplied expression
matches an expected type constraint.
- Add existential operator ``?.`` for ``options`` to enable dot-chaining on optional values

### Language changes

Expand Down
109 changes: 29 additions & 80 deletions lib/pure/options.nim
Original file line number Diff line number Diff line change
Expand Up @@ -146,42 +146,12 @@ proc get*[T](self: var Option[T]): var T =
raise UnpackError(msg: "Can't obtain a value from a `none`")
return self.val

proc map*[T](self: Option[T], callback: proc (input: T)) =
## Applies a callback to the value in this Option
if self.isSome:
callback(self.val)

proc map*[T, R](self: Option[T], callback: proc (input: T): R): Option[R] =
## Applies a callback to the value in this Option and returns an option
## containing the new value. If this option is None, None will be returned
if self.isSome:
some[R]( callback(self.val) )
else:
none(R)

proc flatten*[A](self: Option[Option[A]]): Option[A] =
## Remove one level of structure in a nested Option.
if self.isSome:
self.val
else:
none(A)

proc flatMap*[A, B](self: Option[A], callback: proc (input: A): Option[B]): Option[B] =
## Applies a callback to the value in this Option and returns an
## option containing the new value. If this option is None, None will be
## returned. Similar to ``map``, with the difference that the callback
## returns an Option, not a raw value. This allows multiple procs with a
## signature of ``A -> Option[B]`` (including A = B) to be chained together.
map(self, callback).flatten()

proc filter*[T](self: Option[T], callback: proc (input: T): bool): Option[T] =
## Applies a callback to the value in this Option. If the callback returns
## `true`, the option is returned as a Some. If it returns false, it is
## returned as a None.
if self.isSome and not callback(self.val):
none(T)
else:
self
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: opt.val else: otherwise

proc `==`*(a, b: Option): bool =
## Returns ``true`` if both ``Option``s are ``none``,
Expand Down Expand Up @@ -251,50 +221,6 @@ when isMainModule:
test "$":
check($(some("Correct")) == "Some(\"Correct\")")
check($(stringNone) == "None[string]")

test "map with a void result":
var procRan = 0
some(123).map(proc (v: int) = procRan = v)
check procRan == 123
intNone.map(proc (v: int) = check false)

test "map":
check(some(123).map(proc (v: int): int = v * 2) == some(246))
check(intNone.map(proc (v: int): int = v * 2).isNone)

test "filter":
check(some(123).filter(proc (v: int): bool = v == 123) == some(123))
check(some(456).filter(proc (v: int): bool = v == 123).isNone)
check(intNone.filter(proc (v: int): bool = check false).isNone)

test "flatMap":
proc addOneIfNotZero(v: int): Option[int] =
if v != 0:
result = some(v + 1)
else:
result = none(int)

check(some(1).flatMap(addOneIfNotZero) == some(2))
check(some(0).flatMap(addOneIfNotZero) == none(int))
check(some(1).flatMap(addOneIfNotZero).flatMap(addOneIfNotZero) == some(3))

proc maybeToString(v: int): Option[string] =
if v != 0:
result = some($v)
else:
result = none(string)

check(some(1).flatMap(maybeToString) == some("1"))

proc maybeExclaim(v: string): Option[string] =
if v != "":
result = some v & "!"
else:
result = none(string)

check(some(1).flatMap(maybeToString).flatMap(maybeExclaim) == some("1!"))
check(some(0).flatMap(maybeToString).flatMap(maybeExclaim) == none(string))

test "SomePointer":
var intref: ref int
check(option(intref).isNone)
Expand All @@ -321,3 +247,26 @@ when isMainModule:

let noperson = none(Person)
check($noperson == "None[Person]")

test "either":
check(either(some("Correct"), "Wrong") == "Correct")
check(either(stringNone, "Correct") == "Correct")

test "either without side effect":
var evaluated = 0
proc dummySome(): Option[string] =
evaluated += 1
return some("dummy")
proc dummyStr(): string =
evaluated += 1
return "dummy"
# Check that dummyStr isn't called when we have an option
check(either(some("Correct"), dummyStr()) == "Correct")
check evaluated == 0
# Check that dummyStr is called when we don't have an option
check(either(stringNone, dummyStr()) == "dummy")
check evaluated == 1
evaluated = 0
# Check that dummySome is only called once when used as the some value
check(either(dummySome(), "Wrong") == "dummy")
check evaluated == 1
160 changes: 160 additions & 0 deletions lib/pure/optionsutils.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#
#
# Nim's Runtime Library
# (c) Copyright 2015 Nim Contributors
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#

## This module, previously a part of ``options``, implements some more advanced
## ways to interact with options. It includes conditional mapping of procedures
## over an option, flattening of nested options, and filtering of values within
## options. It also includes an existential operator that works like regular
## dot-chaining but stops if the left hand side is a none-option.

import options, macros

proc map*[T](self: Option[T], callback: proc (input: T)) =
## Applies a callback to the value in this Option
if self.isSome:
callback(self.unsafeGet)

proc map*[T, R](self: Option[T], callback: proc (input: T): R): Option[R] =
## Applies a callback to the value in this Option and returns an option
## containing the new value. If this option is None, None will be returned
if self.isSome:
some[R]( callback(self.unsafeGet) )
else:
none(R)

proc flatten*[A](self: Option[Option[A]]): Option[A] =
## Remove one level of structure in a nested Option.
if self.isSome:
self.unsafeGet
else:
none(A)

proc flatMap*[A, B](self: Option[A], callback: proc (input: A): Option[B]): Option[B] =
## Applies a callback to the value in this Option and returns an
## option containing the new value. If this option is None, None will be
## returned. Similar to ``map``, with the difference that the callback
## returns an Option, not a raw value. This allows multiple procs with a
## signature of ``A -> Option[B]`` (including A = B) to be chained together.
map(self, callback).flatten()

proc filter*[T](self: Option[T], callback: proc (input: T): bool): Option[T] =
## Applies a callback to the value in this Option. If the callback returns
## `true`, the option is returned as a Some. If it returns false, it is
## returned as a None.
if self.isSome and not callback(self.unsafeGet):
none(T)
else:
self

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 ``Option[T]`` where ``T`` is the returned type of ``statements``.
## If nothing is returned from ``statements`` this returns nothing.
##
## .. code-block:: nim
Copy link
Member

@timotheecour timotheecour Oct 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runnableExamples + doAssert's instead of echo's ? (otherwise nothing guarantees it stays in sync or keeps compiling)

## 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
## if some("Hello")?.find('l') == 2:
## echo "This prints"
## if none(string)?.find('l') == 2:
## echo "This doesn't"
let opt = genSym(nskLet)
var
injected = statements
firstBarren = statements
if firstBarren.len != 0:
while true:
if firstBarren[0].len == 0:
firstBarren[0] = nnkDotExpr.newTree(
nnkDotExpr.newTree(opt, newIdentNode("unsafeGet")), firstBarren[0])
break
firstBarren = firstBarren[0]
else:
injected = nnkDotExpr.newTree(
nnkDotExpr.newTree(opt, newIdentNode("unsafeGet")), firstBarren)

result = quote do:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will cause performance problems.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That it's wrapped in a proc? Any idea how to avoid it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this pattern (https://en.wikipedia.org/wiki/Immediately-invoked_function_expression) in Nim can use block expressions:

var i = 0
let a = block:
  if i == 0:
    i+2
  else:
    i*3
echo a

Copy link
Contributor Author

@PMunch PMunch Oct 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic in the current procedure is not that simple though, this is what would be generated for something that returns a value:

(proc (): auto =
  let :tmp130615 = x
  if :tmp130615.isSome:
    return some(:tmp130615.unsafeGet.slice(3)[0]))()

As you can see it uses auto as the type and relies on Nim to create the default none value regardless of the type, so it doesn't have an else branch to the if case. I tried wrapping this up in a block statement, but without luck.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
result = quote do:
result = quote do:
(proc (): auto {.inline.} =

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah making it inline is probably a good idea, but if I apply your suggested cange it would be a double proc wouldn't it? Not sure how this new, fancy, GitHub feature works :P

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh i see, guess I used that new feature wrong myself; maybe just apply the change manually then

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(proc (): auto {.inline.} =
let `opt` = `option`
if `opt`.isSome:
when compiles(`injected`) and not compiles(some(`injected`)):
`injected`
else:
return some(`injected`)
)()

when isMainModule:
Copy link
Member

@timotheecour timotheecour Oct 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toptionsutil.nim? see #9328 (I'm not sure if your test will be run by CI unless something else in testament calls it) /cc @Araq

import unittest, sequtils

suite "optionsutils":
# work around a bug in unittest
let intNone = none(int)
let stringNone = none(string)

test "map with a void result":
var procRan = 0
some(123).map(proc (v: int) = procRan = v)
check procRan == 123
intNone.map(proc (v: int) = check false)

test "map":
check(some(123).map(proc (v: int): int = v * 2) == some(246))
check(intNone.map(proc (v: int): int = v * 2).isNone)

test "filter":
check(some(123).filter(proc (v: int): bool = v == 123) == some(123))
check(some(456).filter(proc (v: int): bool = v == 123).isNone)
check(intNone.filter(proc (v: int): bool = check false).isNone)

test "flatMap":
proc addOneIfNotZero(v: int): Option[int] =
if v != 0:
result = some(v + 1)
else:
result = none(int)

check(some(1).flatMap(addOneIfNotZero) == some(2))
check(some(0).flatMap(addOneIfNotZero) == none(int))
check(some(1).flatMap(addOneIfNotZero).flatMap(addOneIfNotZero) == some(3))

proc maybeToString(v: int): Option[string] =
if v != 0:
result = some($v)
else:
result = none(string)

check(some(1).flatMap(maybeToString) == some("1"))

proc maybeExclaim(v: string): Option[string] =
if v != "":
result = some v & "!"
else:
result = none(string)

check(some(1).flatMap(maybeToString).flatMap(maybeExclaim) == some("1!"))
check(some(0).flatMap(maybeToString).flatMap(maybeExclaim) == none(string))

test "existential operator":
when not compiles(some("Hello world")?.find('w').echo):
check false
check (some("Hello world")?.find('w')).unsafeGet == 6
var evaluated = false
if (some("team")?.find('i')).unsafeGet == -1:
evaluated = true
check evaluated == true
evaluated = false
if (none(string)?.find('i')).isSome:
evaluated = true
check evaluated == false