Skip to content
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

Closed
wants to merge 6 commits into from
Closed
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
262 changes: 262 additions & 0 deletions lib/pure/optionals.nim
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
Copy link
Contributor

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.

Copy link
Contributor Author

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.

## 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"
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't understand.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is an alternative to [] right? A faster alternative, how much faster?

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 =
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't the default == for all objects give the same results?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe, but leaving it out would be similar to exposing implementation detail.

Copy link
Contributor

Choose a reason for hiding this comment

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

How so?

Copy link
Contributor

Choose a reason for hiding this comment

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

{val: 0x38945abc, has: false} is a perfectly valid state, yet {val: 0x38945abc, has: false} != {val: 0x38945ab, has: false} if == is not overridden.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, and flaviu's argument seems good, unless you remember that nothing's value is always all-zeros

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 case in the type definition

Copy link
Contributor

Choose a reason for hiding this comment

The 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) =
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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 just(5) or 2 == 5.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe it's getting parsed as assert just(5) or (2 == 2), although that should be a type error.
I'll look into it once I get to a computer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Must be assert just(5) or (2 == 2). Don't forget converter toBool. 😟
This is a huge argument against naming this operation as or.

Copy link
Contributor

Choose a reason for hiding this comment

The 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 toBool()? It's pretty much useless with ?= anyway.

Copy link
Contributor

Choose a reason for hiding this comment

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

Converters are usually a bad idea. Remove the converter instead of renaming or.

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