-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add optionals module (Maybe[T]) #2515
Changes from all commits
409d229
921d9fa
9a877fb
954c57a
4f25a36
739e5e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,262 @@ | ||
# | ||
# | ||
# Nim's Runtime Library | ||
# (c) Copyright 2015 Nim Contributors | ||
# | ||
# See the file "copying.txt", included in this | ||
# distribution, for details about the copyright. | ||
# | ||
|
||
## :Author: Oleh Prypin | ||
## | ||
## Abstract | ||
## ======== | ||
## | ||
## This module implements types which encapsulate an optional value. | ||
## | ||
## A value of type ``?T`` (``Maybe[T]``) either contains a value `x` | ||
## (represented as ``just(x)``) or is empty (``nothing(T)``). | ||
## | ||
## This can be useful when you have a value that can be present or not. | ||
## The absence of a value is often represented by ``nil``, but it is not always | ||
## available, nor is it always a good solution. | ||
## | ||
## | ||
## Tutorial | ||
## ======== | ||
## | ||
## Let's start with an example: a procedure that finds the index of a character | ||
## in a string. | ||
## | ||
## .. code-block:: nim | ||
## | ||
## import optionals | ||
## | ||
## proc find(haystack: string, needle: char): ?int = | ||
## for i, c in haystack: | ||
## if c == needle: | ||
## return just i | ||
## return nothing(int) # This line is actually optional, | ||
## # because the default is empty | ||
## | ||
## The ``?`` operator (template) is a shortcut for ``Maybe[T]``. | ||
## | ||
## .. code-block:: nim | ||
## | ||
## try: | ||
## assert("abc".find('c')[] == 2) # Immediately extract the value | ||
## except FieldError: # If there is no value | ||
## assert false # This will not be reached, because the value is present | ||
## | ||
## The ``[]`` operator demonstrated above returns the underlying value, or | ||
## raises ``FieldError`` if there is no value. There is another option for | ||
## obtaining the value: ``val``, but you must only use it when you are | ||
## absolutely sure the value is present (e.g. after checking ``has``). If you do | ||
## not care about the tiny overhead that ``[]`` causes, you should simply never | ||
## use ``val``. | ||
## | ||
## How to deal with an absence of a value: | ||
## | ||
## .. code-block:: nim | ||
## | ||
## let result = "team".find('i') | ||
## | ||
## # Nothing was found, so the result is `nothing`. | ||
## assert(result == nothing(int)) | ||
## # It has no value: | ||
## assert(result.has == false) | ||
## # A different way to write it: | ||
## assert(not result) | ||
## | ||
## try: | ||
## echo result[] | ||
## assert(false) # This will not be reached | ||
## except FieldError: # Because an exception is raised | ||
## discard | ||
## | ||
## Now let's try out the extraction template. It returns whether a value | ||
## is present and injects the value into a variable. It is meant to be used in | ||
## a conditional. | ||
## | ||
## .. code-block:: nim | ||
## | ||
## if pos ?= "nim".find('i'): | ||
## assert(pos is int) # This is a normal integer, no tricks. | ||
## echo "Match found at position ", pos | ||
## else: | ||
## assert(false) # This will not be reached | ||
## | ||
## Or maybe you want to get the behavior of the standard library's ``find``, | ||
## which returns `-1` if nothing was found. | ||
## | ||
## .. code-block:: nim | ||
## | ||
## assert(("team".find('i') or -1) == -1) | ||
## assert(("nim".find('i') or -1) == 1) | ||
|
||
import typetraits | ||
|
||
|
||
type | ||
Maybe*[T] = object | ||
## An optional type that stores its value and state separately in a boolean. | ||
val: T | ||
has: bool | ||
|
||
|
||
template `?`*(T: typedesc): typedesc = | ||
## ``?T`` is equivalent to ``Maybe[T]``. | ||
Maybe[T] | ||
|
||
|
||
proc just*[T](val: T): Maybe[T] = | ||
## Returns a ``Maybe`` that has this value. | ||
result.has = true | ||
result.val = val | ||
|
||
proc nothing*(T: typedesc): Maybe[T] = | ||
## Returns a ``Maybe`` for this type that has no value. | ||
result.has = false | ||
|
||
|
||
proc has*(maybe: Maybe): bool = | ||
## Returns ``true`` if `maybe` isn't `nothing`. | ||
maybe.has | ||
|
||
converter toBool*(maybe: Maybe): bool = | ||
## Same as ``has``. Allows to use a ``Maybe`` in boolean context. | ||
maybe.has | ||
|
||
|
||
proc unsafeVal*[T](maybe: Maybe[T]): T = | ||
## Returns the value of a `just`. Behavior is undefined for `nothing`. | ||
assert maybe.has, "nothing has no val" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this really warrant a proc? How much speed does it actually gain? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is an alternative to |
||
maybe.val | ||
|
||
proc `[]`*[T](maybe: Maybe[T]): T = | ||
## Returns the value of `maybe`. Raises ``FieldError`` if it is `nothing`. | ||
if not maybe: | ||
raise newException(FieldError, "Can't obtain a value from a `nothing`") | ||
maybe.val | ||
|
||
|
||
template `or`*[T](maybe: Maybe[T], default: T): T = | ||
## Returns the value of `maybe`, or `default` if it is `nothing`. | ||
if maybe: maybe.val | ||
else: default | ||
|
||
template `or`*[T](a, b: Maybe[T]): Maybe[T] = | ||
## Returns `a` if it is `just`, otherwise `b`. | ||
if a: a | ||
else: b | ||
|
||
template `?=`*(into: expr, maybe: Maybe): bool = | ||
## Returns ``true`` if `maybe` isn't `nothing`. | ||
## | ||
## Injects a variable with the name specified by the argument `into` | ||
## with the value of `maybe`, or its type's default value if it is `nothing`. | ||
## | ||
## .. code-block:: nim | ||
## | ||
## proc message(): ?string = | ||
## just "Hello" | ||
## | ||
## if m ?= message(): | ||
## echo m | ||
var into {.inject.}: type(maybe.val) | ||
if maybe: | ||
into = maybe.val | ||
maybe | ||
|
||
|
||
proc `==`*(a, b: Maybe): bool = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't the default There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe, but leaving it out would be similar to exposing implementation detail. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How so? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we want to change the implementation to something that doesn't coincidentally have the same == behavior, we would need to add this proc, thus changing the public API. Of course, it is ridiculous to sweat over something like this, but I don't think this should be removed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, and flaviu's argument seems good, unless you remember that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that I think about this, maybe this additional zeroing wouldn't need to happen if I used a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I disagree about that being a valid state. Is that state even possible with the current API? |
||
## Returns ``true`` if both ``Maybe`` are `nothing`, | ||
## or if they have equal values | ||
(a.has and b.has and a.val == b.val) or (not a.has and not b.has) | ||
|
||
proc `$`[T](maybe: Maybe[T]): string = | ||
## Converts to string: `"just(value)"` or `"nothing(type)"` | ||
if maybe.has: | ||
"just(" & $maybe.val & ")" | ||
else: | ||
"nothing(" & T.name & ")" | ||
|
||
|
||
when isMainModule: | ||
template expect(E: expr, body: stmt) = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of redefining this, it'd be possible to use the unittest module. |
||
try: | ||
body | ||
assert false, E.type.name & " not raised" | ||
except E: | ||
discard | ||
|
||
|
||
block: # example | ||
proc find(haystack: string, needle: char): ?int = | ||
for i, c in haystack: | ||
if c == needle: | ||
return just i | ||
|
||
assert("abc".find('c')[] == 2) | ||
|
||
let result = "team".find('i') | ||
|
||
assert result == nothing(int) | ||
assert result.has == false | ||
|
||
if pos ?= "nim".find('i'): | ||
assert pos is int | ||
assert pos == 1 | ||
else: | ||
assert false | ||
|
||
assert(("team".find('i') or -1) == -1) | ||
assert(("nim".find('i') or -1) == 1) | ||
|
||
block: # just | ||
assert just(6)[] == 6 | ||
assert just("a").unsafeVal == "a" | ||
assert just(6).has | ||
assert just("a") | ||
|
||
block: # nothing | ||
expect FieldError: | ||
discard nothing(int)[] | ||
assert(not nothing(int).has) | ||
assert(not nothing(string)) | ||
|
||
block: # equality | ||
assert just("a") == just("a") | ||
assert just(7) != just(6) | ||
assert just("a") != nothing(string) | ||
assert nothing(int) == nothing(int) | ||
|
||
when compiles(just("a") == just(5)): | ||
assert false | ||
when compiles(nothing(string) == nothing(int)): | ||
assert false | ||
|
||
block: # stringification | ||
assert "just(7)" == $just(7) | ||
assert "nothing(int)" == $nothing(int) | ||
|
||
block: # or | ||
assert just(1) or just(2) == just(1) | ||
assert nothing(string) or just("a") == just("a") | ||
assert nothing(int) or nothing(int) == nothing(int) | ||
assert just(5) or 2 == 2 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, this is weird. Traditionally, if we already have a value, injecting a default should not have any effect. I would expect There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe it's getting parsed as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Must be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, good point. That's how it typechecks. assert just(5) or 2 == 2
assert just(5) or (2 == 2)
assert toBool(just(5)) or (2 == 2)
assert true or true Isn't it possible to just get rid of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Converters are usually a bad idea. Remove the converter instead of renaming |
||
assert nothing(string) or "a" == "a" | ||
|
||
when compiles(just(1) or "2"): | ||
assert false | ||
when compiles(nothing(int) or just("a")): | ||
assert false | ||
|
||
block: # extraction template | ||
if a ?= just(5): | ||
assert a == 5 | ||
else: | ||
assert false | ||
|
||
if b ?= nothing(string): | ||
assert false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is using
val
possible? It's not exported, so the docs shouldn't imply this.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Woops, forgot to change that.