Compare commits

...

75 Commits
0.9.1 ... main

Author SHA1 Message Date
Adam Uhlíř
2906828765
Add errorOption example to Readme
Added example for errorOption conversion in Readme.
2025-10-22 16:12:58 +02:00
Marcin Czenko
2e7f20392b Update Readme.md
Missing "to"
2024-12-04 01:01:47 +01:00
Mark Spanbroek
3dcf21491d without with error variable works for any Result type 2024-05-29 12:45:40 +02:00
Mark Spanbroek
82d90b67bc version 0.10.15 2024-04-20 08:08:31 +02:00
Mark Spanbroek
b098ae696a Workaround for Nim gensym bug
Occasionally different `evaluated` symbols would be
gensymmed to the same symbol.
2024-04-20 08:07:28 +02:00
Mark Spanbroek
83ae4a6409 version 0.10.14 2024-03-10 12:14:59 +01:00
Mark Spanbroek
57e467b8b0 Fix: without should work when $ has side effects
It's ok to use unsafeError in bindFailed, because we've
already checked that the result contains an error.
2024-03-10 12:13:25 +01:00
Mark Spanbroek
47692e0d92 version 0.10.13 2024-01-09 16:57:24 +01:00
Mark Spanbroek
6ef525cfe2 Reference types are handled by without statement with error 2024-01-09 16:52:39 +01:00
Mark Spanbroek
43e7deb827 Fix BareExcept warnings 2024-01-09 16:46:01 +01:00
Mark Spanbroek
4a74d65e17 Mark generated error variable explicitly as {.gensym.}
Co-Authored-By: Jaremy Creechley <creechley@gmail.com>
2024-01-09 16:45:32 +01:00
Mark Spanbroek
672248f431 Get rid of trick with type parameter 2024-01-09 16:45:32 +01:00
Mark Spanbroek
d463d491cc Handle bind (=?) errors in without statements differently
Keeps track of the current error variable at compile time,
instead of using a pointer to the error variable at runtime.

Employs a trick with an unused type parameter to ensure that
invocations of the bindFailed() macro are expanded after
captureBindError() is expanded.
2024-01-09 16:45:32 +01:00
Tomasz Bekas
1f0afff48b
Support for .?[] operator on openArrays (#52)
* Support for .?[] operator on openArrays

* Operator .?[] evaluates openArray expression only once

* Fix for Nim 1.2.x

---------

Co-authored-by: Mark Spanbroek <mark@spanbroek.net>
2023-11-20 14:58:49 +01:00
Mark Spanbroek
2dd6b6b220 version 0.10.12 2023-11-14 10:57:03 +01:00
Nickolay Bukreyev
0f095d6b7c Overload toOption for Result
Necessary for the `=?` operator to work.
2023-11-14 10:52:45 +01:00
Nickolay Bukreyev
cdf639c4ea Support binding closure iterators (except on Nim == 2.0)
See nim-lang/Nim#22932.
2023-11-14 10:52:45 +01:00
Nickolay Bukreyev
fe47a19825 Create identifiers with genSym 2023-11-14 10:52:45 +01:00
Nickolay Bukreyev
440debc7c3 Accept only optional and reference types as RHS of =?
The fact that `option(x)` works for non-reference types (being an alias
for `some`) is an stdlib's design mistake that bites us here.
2023-11-14 10:52:45 +01:00
Mark Spanbroek
c2a08bd703 version 0.10.11 2023-11-08 10:14:25 +01:00
Mark Spanbroek
52e11f2011 Fix compilation issue with Nim 1.6.16
Workaround for https://github.com/nim-lang/Nim/issues/22897
2023-11-08 10:09:10 +01:00
Mark Spanbroek
af4f194597 Fix tests for Nim < 2.0 2023-08-30 11:02:37 +02:00
Mark Spanbroek
5c8d422ac8 fix error binding in without statement on multiple threads 2023-08-30 11:02:37 +02:00
Mark Spanbroek
e56cf86c4a Indexing of strings and sequences should not catch Defect
Catching a Defect does not always work, depending on how
it's compiled.
2023-08-03 15:17:40 +02:00
Tomasz Bekas
416b6dd566 Support seq indexing 2023-08-03 12:56:12 +02:00
Mark Spanbroek
08581f5efd Run CI on Nim 1.2, 1.4, 1.6 and 2.0 2023-08-03 09:41:43 +02:00
Mark Spanbroek
9af4ce1ca1 Better compilation error when calling without with wrong parameter 2023-08-03 09:41:43 +02:00
Mark Spanbroek
1569ef4526 Fix tests for stew with Nim < 1.6
Stew now requires a Nim >= 1.6, so this disables
testing stew with earlier Nim versions.
2023-07-28 09:57:51 +02:00
Mark Spanbroek
b3cf35ac45 version 0.10.10 2023-07-03 11:17:51 +02:00
Mark Spanbroek
3bce3088a7 Use root configuration when running tests 2023-07-03 11:16:56 +02:00
Mark Spanbroek
ffe0faa3bb Only enable --styleCheck:usages on Nim versions that support it 2023-07-03 11:16:56 +02:00
Mark Spanbroek
0d7ce8efde Provide conversion to string $ for Results 2023-06-26 10:04:02 +02:00
Mark Spanbroek
58c10fb333 version 0.10.9 2023-06-26 10:02:25 +02:00
Mark Spanbroek
8daae27089 Do not edit AST nodes, make a copy first
Fixes error "typechecked nodes may not be modified"
2023-06-26 09:59:14 +02:00
Mark Spanbroek
b18444a6d0 Support renamed 'results' library 2023-06-05 17:26:46 +02:00
Mark Spanbroek
f957dd59d6 Fix tests on Nim 1.6.12
Ignore the nimble.lock at the root level when running tests
2023-06-05 17:26:46 +02:00
Mark Spanbroek
6cbbda7e4d version 0.10.8 2023-02-14 09:31:58 +01:00
Eric Mastro
096ca864b0
tuple binding and unpacking support (#29)
* tuple binding and unpacking support

* support tuple binding in nim < 1.6

* Use ? instead of Option[] in tests

* Test binding of optional tuple, not just tuple

* Add tests for tuple unpacking of Result

* Remove unused import

* Rearrange tuple binding code

Introduce separate proc for creation of unpacking
statement.

Use `quote do` to make code as similar as possible
to the `bindLet` and `bindVar` templates.

* Add tests for failed tuple bindings

---------

Co-authored-by: Mark Spanbroek <mark@spanbroek.net>
2023-02-14 09:56:32 +11:00
Mark Spanbroek
1dcef4b302 version 0.10.7 2022-10-20 12:13:43 +02:00
Mark Spanbroek
f78bdd9d58 Fix redefinition of 'T`gensymXX' error
In rare instances, the Nim compiler will generate the same
symbol more than once. Adding a block works around this issue.

Reproducing this behavior in a unit test has proved elusive.
2022-10-20 05:10:44 -05:00
Mark Spanbroek
30e4184a99 version 0.10.6 2022-09-28 11:37:36 +02:00
Mark Spanbroek
4d631b1ba9 Fix: ensure that options and results are only evaluated once 2022-09-28 11:36:28 +02:00
Mark Spanbroek
82408a5ca2 version 0.10.5 2022-08-10 13:39:00 +02:00
Mark Spanbroek
cbdd882b9f Fix: unused variable 2022-08-10 13:35:47 +02:00
Mark Spanbroek
9e3a822877 Fix: disregard early symbol lookup for error variable 2022-08-10 13:35:47 +02:00
Mark Spanbroek
cfe17ca899 Fix error when using an existing name as error variable 2022-08-10 13:35:47 +02:00
Mark Spanbroek
90ea780ba9 Fix: nested without calls in generic code 2022-08-10 13:29:09 +02:00
Mark Spanbroek
4abeef5c36 Fix: without statement with error works in nested calls 2022-08-10 13:29:09 +02:00
Mark Spanbroek
22f2c9761a Disable warning about DotLikeOps
Give a clear compiler error when questionable
is used with the -d:nimPreviewDotLikeOps flag.

Reason: the option is likely to be deprecated
or removed. More info:
https://github.com/nim-lang/Nim/pull/19919
2022-08-10 13:28:52 +02:00
Mark Spanbroek
13c7ff7671 Retrieve optional error from Result 2022-08-04 13:52:07 +02:00
Tanguy
b0666ba4f1
Fixes for styleCheck:usages (#16)
* Fixes for styleCheck:usages
* Bump nim 1.2 in CI
2022-08-03 15:21:00 +02:00
Ivan Yonchovski
955597a4fd
Add setup/lock files (#14) 2022-07-12 21:34:52 +03:00
Mark Spanbroek
0f9b12abc6 version 0.10.4 2022-07-04 09:59:59 +02:00
Mark Spanbroek
d9333a8f03 Remove obsolete ASDF tool version 2022-07-04 09:56:56 +02:00
Mark Spanbroek
0895a9c065 Fix: without statement with error works in generic code 2022-07-04 09:53:02 +02:00
Mark Spanbroek
d7e9f0bf7f Version 0.10.3 2022-04-04 09:56:53 +02:00
Nickolay Bukreyev
361948499b Fix deprecation warning when importing errorban inside the library
`errorban` module [has been deprecated][deprecation] since v0.5.0.
The library continued to use it internally, but because of that, it
was triggering its own deprecation warning. This commit splits
`errorban.nim` into two pieces: the warning and actual code.

[deprecation]: https://github.com/status-im/questionable/commit/e66cd2439b
2022-04-04 09:52:01 +02:00
Mark Spanbroek
6018fd43e0 version 0.10.2 2022-01-12 17:57:03 +01:00
Mark Spanbroek
a748d22350 Fix: ensure that overridden =? operator does not remain in scope 2022-01-12 17:54:24 +01:00
Mark Spanbroek
ef29000f94 Without statement for Results provides access to errors 2022-01-12 17:54:24 +01:00
Mark Spanbroek
91a38040ea Add license 2022-01-10 11:04:48 +01:00
Mark Spanbroek
7b209af6fb version 0.10.1 2021-12-04 17:56:14 +01:00
Mark Spanbroek
92641b552d Switch warningAsError only works for Nim >= 1.4.0 2021-12-04 17:56:14 +01:00
Mark Spanbroek
cfe4c6fc95 Ensure that =? works with types that do not have a default value 2021-12-04 17:26:08 +01:00
Mark Spanbroek
d7a757a8cb Switch to Nim 1.6.0 for development 2021-11-01 17:18:49 +01:00
Mark Spanbroek
0fe40d2347 Run CI on Nim 1.2, 1.4 and 1.6 2021-11-01 17:18:35 +01:00
Mark Spanbroek
56a4bca641 Fix typo 2021-08-30 18:13:43 +02:00
Mark Spanbroek
4967084d22 Better errors when using a proc without a return type in a .? chain 2021-08-30 15:58:29 +02:00
Mark Spanbroek
b9a090d001 Ensure that test modules use local nimbledeps folder
Even when NIMBLE_DIR is set externally.
2021-07-12 11:26:35 +02:00
Mark Spanbroek
8feb684574 Fix "unused variable" warnings 2021-07-07 09:19:44 +02:00
Mark Spanbroek
4a1783c472 Add documentation comments 2021-06-04 17:34:48 +02:00
Mark Spanbroek
a2ded4f01a Simplify 2021-06-04 16:38:44 +02:00
Mark Spanbroek
986716511f version 0.10.0 2021-05-31 16:26:06 +02:00
Mark Spanbroek
3f18970931 Nim 1.4.8 2021-05-31 16:24:30 +02:00
Mark Spanbroek
93f5c919fb Ensure that optional binding works with closures 2021-05-31 16:24:21 +02:00
37 changed files with 1369 additions and 390 deletions

View File

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
nim: [stable, 1.2.6] nim: [stable, 1.6.16, 1.4.8, 1.2.18]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: iffy/install-nim@v3 - uses: iffy/install-nim@v3

2
.gitignore vendored
View File

@ -2,3 +2,5 @@
!*/ !*/
!*.* !*.*
nimbledeps nimbledeps
nimble.develop
nimble.paths

View File

@ -1 +0,0 @@
nim 1.4.6

5
License.md Normal file
View File

@ -0,0 +1,5 @@
Licensed and distributed under either of
[MIT license](http://opensource.org/licenses/MIT) or
[Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
at your option. These files may not be copied, modified, or distributed except
according to those terms.

View File

@ -12,14 +12,14 @@ Use the [Nimble][3] package manager to add `questionable` to an existing
project. Add the following to its .nimble file: project. Add the following to its .nimble file:
```nim ```nim
requires "questionable >= 0.9.1 & < 0.10.0" requires "questionable >= 0.10.15 & < 0.11.0"
``` ```
If you want to make use of Result types, then you also have to add either the If you want to make use of Result types, then you also have to add either the
[result][2] package, or the [stew][4] package: [result][2] package, or the [stew][4] package:
```nim ```nim
requires "result" # either this requires "results" # either this
requires "stew" # or this requires "stew" # or this
``` ```
@ -63,7 +63,7 @@ else:
# this is reached, and y is not defined # this is reached, and y is not defined
``` ```
The `without` statement can be used to place guards that ensure that an optional The `without` statement can be used to place guards that ensure that an Option
contains a value: contains a value:
```nim ```nim
@ -151,7 +151,7 @@ have to explicitly import the `questionable/results` module:
import questionable/results import questionable/results
``` ```
You can use `?!` make a Result type. These Result types either hold a value or You can use `?!` to make a Result type. These Result types either hold a value or
an error. For example the type `?!int` is short for `Result[int, ref an error. For example the type `?!int` is short for `Result[int, ref
CatchableError]`. CatchableError]`.
@ -194,6 +194,20 @@ let value = fails() |? @[]
let sum = works()[3] + 40 let sum = works()[3] + 40
``` ```
### Without statement
The `without` statement can also be used with Results. It provides access to any
errors that may arise:
```nim
proc someProc(r: ?!int) =
without value =? r, error:
# use `error` to get the error from r
return
# use value
```
### Catching errors ### Catching errors
When you want to use Results, but need to call a proc that may raise an When you want to use Results, but need to call a proc that may raise an
@ -212,6 +226,7 @@ Any Result can be converted to an Option:
```nim ```nim
let converted = works().option # equals @[1, 1, 2, 2, 2].some let converted = works().option # equals @[1, 1, 2, 2, 2].some
let errOption = fails().errorOption # option that is set when the Result holds an error
``` ```
[1]: https://nim-lang.org/docs/options.html [1]: https://nim-lang.org/docs/options.html

16
config.nims Normal file
View File

@ -0,0 +1,16 @@
# Style Check
if (NimMajor, NimMinor, NimPatch) >= (1, 6, 6):
--styleCheck:usages
if (NimMajor, NimMinor) < (1, 6):
--styleCheck:hint
else:
--styleCheck:error
# Disable some warnings
if (NimMajor, NimMinor) >= (1, 6):
switch("warning", "DotLikeOps:off")
# begin Nimble config (version 1)
when fileExists("nimble.paths"):
include "nimble.paths"
# end Nimble config

4
nimble.lock Normal file
View File

@ -0,0 +1,4 @@
{
"version": 1,
"packages": {}
}

View File

@ -1,3 +1,4 @@
import ./questionable/dotlike
import ./questionable/options import ./questionable/options
export options export options

View File

@ -1,10 +1,11 @@
version = "0.9.1" version = "0.10.15"
author = "Questionable Authors" author = "Questionable Authors"
description = "Elegant optional types" description = "Elegant optional types"
license = "MIT" license = "MIT"
task test, "Runs the test suite": task test, "Runs the test suite":
for module in ["options", "result", "stew"]: for module in ["options", "results", "stew"]:
withDir "testmodules/" & module: withDir "testmodules/" & module:
delEnv "NIMBLE_DIR" # use nimbledeps dir
exec "nimble install -d -y" exec "nimble install -d -y"
exec "nimble test -y" exec "nimble test -y"

View File

@ -1,25 +1,89 @@
import std/options import std/options
import std/macros import std/macros
import ./private/binderror
proc option[T](option: Option[T]): Option[T] = when (NimMajor, NimMinor) < (1, 1):
type SomePointer = ref | ptr | pointer
elif (NimMajor, NimMinor) == (2, 0): # Broken in 2.0.0, fixed in 2.1.1.
type SomePointer = ref | ptr | pointer | proc
else:
type SomePointer = ref | ptr | pointer | proc | iterator {.closure.}
template toOption[T](option: Option[T]): Option[T] =
option option
template bindLet(name, expression): bool = template toOption[T: SomePointer](value: T): Option[T] =
let option = expression.option value.option
template name: auto {.used.} = option.unsafeGet()
proc placeholder(T: type): T =
discard
template bindLet(name, expression): untyped =
let evaluated = expression
let option = evaluated.toOption
type T = typeof(option.unsafeGet())
let name {.used.} = if option.isSome:
option.unsafeGet()
else:
bindFailed(evaluated)
placeholder(T)
option.isSome option.isSome
template bindVar(name, expression): bool = template bindVar(name, expression): untyped =
let option = expression.option let evaluated = expression
var name : typeof(option.unsafeGet()) let option = evaluated.toOption
if option.isSome: type T = typeof(option.unsafeGet())
name = option.unsafeGet() var name {.used.} = if option.isSome:
option.unsafeGet()
else:
bindFailed(evaluated)
placeholder(T)
option.isSome option.isSome
proc newUnpackTupleNode(names: NimNode, value: NimNode): NimNode =
# builds tuple unpacking statement, eg: let (a, b) = value
let vartuple = nnkVarTuple.newTree()
for i in 0..<names.len:
vartuple.add names[i]
vartuple.add newEmptyNode()
vartuple.add value
nnkLetSection.newTree(vartuple)
macro bindTuple(names, expression): bool =
let opt = genSym(nskLet, "option")
let evaluated = genSym(nskLet, "evaluated")
let T = genSym(nskType, "T")
let value = quote do:
if `opt`.isSome:
`opt`.unsafeGet()
else:
bindFailed(`evaluated`)
placeholder(`T`)
let letsection = newUnpackTupleNode(names, value)
quote do:
let `evaluated` = `expression`
let `opt` = `evaluated`.toOption
type `T` = typeof(`opt`.unsafeGet())
`letsection`
`opt`.isSome
macro `=?`*(name, expression): bool = macro `=?`*(name, expression): bool =
name.expectKind({nnkIdent, nnkVarTy}) ## The `=?` operator lets you bind the value inside an Option or Result to a
## new variable. It can be used inside of a conditional expression, for
## instance in an `if` statement.
when (NimMajor, NimMinor) < (1, 6):
name.expectKind({nnkIdent, nnkVarTy, nnkTupleConstr, nnkPar})
else:
name.expectKind({nnkIdent, nnkVarTy, nnkTupleConstr})
if name.kind == nnkIdent: if name.kind == nnkIdent:
quote do: bindLet(`name`, `expression`) quote do: bindLet(`name`, `expression`)
elif name.kind == nnkTupleConstr or name.kind == nnkPar:
quote do: bindTuple(`name`, `expression`)
else: else:
let name = name[0] let name = name[0]
quote do: bindVar(`name`, `expression`) quote do: bindVar(`name`, `expression`)

View File

@ -1,5 +1,6 @@
import std/options import std/options
import std/macros import std/macros
import std/strformat
func isSym(node: NimNode): bool = func isSym(node: NimNode): bool =
node.kind in {nnkSym, nnkOpenSymChoice, nnkClosedSymChoice} node.kind in {nnkSym, nnkOpenSymChoice, nnkClosedSymChoice}
@ -7,31 +8,41 @@ func isSym(node: NimNode): bool =
func expectSym(node: NimNode) = func expectSym(node: NimNode) =
node.expectKind({nnkSym, nnkOpenSymChoice, nnkClosedSymChoice}) node.expectKind({nnkSym, nnkOpenSymChoice, nnkClosedSymChoice})
template `.?`*(option: typed, identifier: untyped{nkIdent}): untyped = macro expectReturnType(identifier: untyped, expression: untyped): untyped =
let message =
fmt"'{identifier}' doesn't have a return type, it can't be in a .? chain"
quote do:
when compiles(`expression`) and not compiles(typeof `expression`):
{.error: `message`.}
template chain(option: typed, identifier: untyped{nkIdent}): untyped =
# chain is of shape: option.?identifier # chain is of shape: option.?identifier
when not compiles(typeof(option.unsafeGet.identifier)): expectReturnType(identifier, option.unsafeGet.identifier)
{.error: ".? chain cannot return void".}
option ->? option.unsafeGet.identifier option ->? option.unsafeGet.identifier
macro `.?`*(option: typed, infix: untyped{nkInfix}): untyped = macro chain(option: typed, infix: untyped{nkInfix}): untyped =
# chain is of shape: option.?left `operator` right # chain is of shape: option.?left `operator` right
let infix = infix.copyNimTree()
let left = infix[1] let left = infix[1]
infix[1] = quote do: `option`.?`left` infix[1] = quote do: `option`.?`left`
infix infix
macro `.?`*(option: typed, bracket: untyped{nkBracketExpr}): untyped = macro chain(option: typed, bracket: untyped{nkBracketExpr}): untyped =
# chain is of shape: option.?left[right] # chain is of shape: option.?left[right]
let bracket = bracket.copyNimTree()
let left = bracket[0] let left = bracket[0]
bracket[0] = quote do: `option`.?`left` bracket[0] = quote do: `option`.?`left`
bracket bracket
macro `.?`*(option: typed, dot: untyped{nkDotExpr}): untyped = macro chain(option: typed, dot: untyped{nkDotExpr}): untyped =
# chain is of shape: option.?left.right # chain is of shape: option.?left.right
let dot = dot.copyNimTree()
let left = dot[0] let left = dot[0]
dot[0] = quote do: `option`.?`left` dot[0] = quote do: `option`.?`left`
dot dot
macro `.?`*(option: typed, call: untyped{nkCall}): untyped = macro chain(option: typed, call: untyped{nkCall}): untyped =
let call = call.copyNimTree()
let procedure = call[0] let procedure = call[0]
if call.len == 1: if call.len == 1:
# chain is of shape: option.?procedure() # chain is of shape: option.?procedure()
@ -50,9 +61,19 @@ macro `.?`*(option: typed, call: untyped{nkCall}): untyped =
else: else:
# chain is of shape: option.?procedure(arguments) # chain is of shape: option.?procedure(arguments)
call.insert(1, quote do: `option`.unsafeGet) call.insert(1, quote do: `option`.unsafeGet)
quote do: `option` ->? `call` quote do:
expectReturnType(`procedure`, `call`)
`option` ->? `call`
macro `.?`*(option: typed, symbol: untyped): untyped = macro chain(option: typed, symbol: untyped): untyped =
symbol.expectSym() symbol.expectSym()
let expression = ident($symbol) let expression = ident($symbol)
quote do: `option`.?`expression` quote do: `option`.?`expression`
template `.?`*(left: typed, right: untyped): untyped =
## The `.?` chaining operator is used to safely access fields and call procs
## on Options or Results. The expression is only evaluated when the preceding
## Option or Result has a value.
block:
let evaluated = left
chain(evaluated, right)

2
questionable/dotlike.nim Normal file
View File

@ -0,0 +1,2 @@
when defined(nimPreviewDotLikeOps):
{.error: "DotLikeOps are not supported by questionable".}

View File

@ -1,10 +1,3 @@
{.warning: "errorban is deprecated; use nimble package `upraises` instead".} {.warning: "errorban is deprecated; use nimble package `upraises` instead".}
## Include this file to indicate that your module does not raise Errors. include ./private/errorban
## Disables compiler hints about unused declarations in Nim < 1.4.0
when (NimMajor, NimMinor, NimPatch) >= (1, 4, 0):
{.push raises:[].}
else:
{.push raises: [Defect].}
{.hint[XDeclaredButNotUsed]: off.}

View File

@ -1,11 +1,32 @@
import std/macros import std/macros
import std/options
proc safeGet[T](expression: seq[T] | openArray[T], index: int): Option[T] =
if index >= expression.low and index <= expression.high:
expression[index].some
else:
T.none
proc safeGet(expression: string, index: int): Option[char] =
if index >= expression.low and index <= expression.high:
expression[index].some
else:
char.none
macro `.?`*(expression: seq | string | openArray, brackets: untyped{nkBracket}): untyped =
# chain is of shape: (seq or string or openArray).?[index]
let index = brackets[0]
quote do:
block:
safeGet(`expression`, `index`)
macro `.?`*(expression: typed, brackets: untyped{nkBracket}): untyped = macro `.?`*(expression: typed, brackets: untyped{nkBracket}): untyped =
# chain is of shape: expression.?[index] # chain is of shape: expression.?[index]
let index = brackets[0] let index = brackets[0]
quote do: quote do:
type T = typeof(`expression`[`index`]) block:
try: type T = typeof(`expression`[`index`])
`expression`[`index`].some try:
except KeyError: `expression`[`index`].some
T.none except KeyError:
T.none

View File

@ -1,12 +1,19 @@
template liftUnary*(T: type, operator: untyped) = template liftUnary*(T: type, operator: untyped) =
template `operator`*(a: T): untyped = template `operator`*(a: T): untyped =
a ->? `operator`(a.unsafeGet()) block:
let evaluated = a
evaluated ->? `operator`(evaluated.unsafeGet())
template liftBinary*(T: type, operator: untyped) = template liftBinary*(T: type, operator: untyped) =
template `operator`*(a: T, b: T): untyped = template `operator`*(a: T, b: T): untyped =
(a, b) ->? `operator`(a.unsafeGet, b.unsafeGet) block:
let evalA = a
let evalB = b
(evalA, evalB) ->? `operator`(evalA.unsafeGet, evalB.unsafeGet)
template `operator`*(a: T, b: typed): untyped = template `operator`*(a: T, b: typed): untyped =
a ->? `operator`(a.unsafeGet(), b) block:
let evalA = a
evalA ->? `operator`(evalA.unsafeGet(), b)

View File

@ -6,7 +6,7 @@ import ./indexing
import ./operators import ./operators
import ./without import ./without
include ./errorban include ./private/errorban
export options except get export options except get
export binding export binding
@ -15,16 +15,16 @@ export indexing
export without export without
template `?`*(T: typed): type Option[T] = template `?`*(T: typed): type Option[T] =
## Use `?` to make a type optional. For example the type `?int` is short for
## `Option[int]`.
Option[T] Option[T]
template `!`*[T](option: ?T): T = template `!`*[T](option: ?T): T =
option.get ## Returns the value of an Option when you're absolutely sure that it
## contains value. Using `!` on an Option without a value raises a Defect.
template `->?`*[T,U](option: ?T, expression: U): ?U = option.get
if option.isSome:
expression.some
else:
U.none
template `->?`*[T,U](option: ?T, expression: ?U): ?U = template `->?`*[T,U](option: ?T, expression: ?U): ?U =
if option.isSome: if option.isSome:
@ -32,11 +32,8 @@ template `->?`*[T,U](option: ?T, expression: ?U): ?U =
else: else:
U.none U.none
template `->?`*[T,U,V](options: (?T, ?U), expression: V): ?V = template `->?`*[T,U](option: ?T, expression: U): ?U =
if options[0].isSome and options[1].isSome: option ->? expression.some
expression.some
else:
V.none
template `->?`*[T,U,V](options: (?T, ?U), expression: ?V): ?V = template `->?`*[T,U,V](options: (?T, ?U), expression: ?V): ?V =
if options[0].isSome and options[1].isSome: if options[0].isSome and options[1].isSome:
@ -44,7 +41,13 @@ template `->?`*[T,U,V](options: (?T, ?U), expression: ?V): ?V =
else: else:
V.none V.none
template `|?`*[T](option: ?T, fallback: T): T = template `->?`*[T,U,V](options: (?T, ?U), expression: V): ?V =
options ->? expression.some
proc `|?`*[T](option: ?T, fallback: T): T =
## Use the `|?` operator to supply a fallback value when an Option does not
## hold a value.
if option.isSome: if option.isSome:
option.unsafeGet() option.unsafeGet()
else: else:
@ -53,11 +56,13 @@ template `|?`*[T](option: ?T, fallback: T): T =
macro `.?`*[T](option: ?T, brackets: untyped{nkBracket}): untyped = macro `.?`*[T](option: ?T, brackets: untyped{nkBracket}): untyped =
let index = brackets[0] let index = brackets[0]
quote do: quote do:
type U = typeof(`option`.unsafeGet().?[`index`].unsafeGet()) block:
if `option`.isSome: let evaluated = `option`
`option`.unsafeGet().?[`index`] type U = typeof(evaluated.unsafeGet().?[`index`].unsafeGet())
else: if evaluated.isSome:
U.none evaluated.unsafeGet().?[`index`]
else:
U.none
Option.liftUnary(`-`) Option.liftUnary(`-`)
Option.liftUnary(`+`) Option.liftUnary(`+`)

View File

@ -0,0 +1,6 @@
template ignoreBareExceptWarning*(body) =
when defined(nimHasWarnBareExcept):
{.push warning[BareExcept]:off warning[UnreachableCode]:off.}
body
when defined(nimHasWarnBareExcept):
{.pop.}

View File

@ -0,0 +1,54 @@
import std/options
import std/macros
# A stack of names of error variables. Keeps track of the error variables that
# are given to captureBindError().
var errorVariableNames {.global, compileTime.}: seq[string]
macro captureBindError*(error: var ref CatchableError, expression): auto =
## Ensures that an error is assigned to the error variable when a binding (=?)
## fails inside the expression.
# name of the error variable as a string literal
let errorVariableName = newLit($error)
let evaluated = genSym(nskLet, "evaluated")
quote do:
# add error variable to the top of the stack
static: errorVariableNames.add(`errorVariableName`)
# evaluate the expression
let `evaluated` = `expression`
# pop error variable from the stack
static: discard errorVariableNames.pop()
# return the evaluated result
`evaluated`
func unsafeCatchableError[T](_: Option[T]): ref CatchableError =
newException(ValueError, "Option is set to `none`")
func unsafeCatchableError[T](_: ref T): ref CatchableError =
newException(ValueError, "ref is nil")
func unsafeCatchableError[T](_: ptr T): ref CatchableError =
newException(ValueError, "ptr is nil")
func unsafeCatchableError[Proc: proc | iterator](_: Proc): ref CatchableError =
newException(ValueError, "proc or iterator is nil")
macro bindFailed*(expression: typed) =
## Called when a binding (=?) fails.
## Assigns an error to the error variable (specified in captureBindError())
## when appropriate.
# The `expression` parameter is typed to ensure that the compiler does not
# expand bindFailed() before it expands invocations of captureBindError().
# check that we have an error variable on the stack
if errorVariableNames.len > 0:
# create an identifier that references the current error variable
let errorVariable = ident errorVariableNames[^1]
return quote do:
# check that the error variable is in scope
when compiles(`errorVariable`):
# assign bind error to error variable
`errorVariable` = `expression`.unsafeCatchableError

View File

@ -0,0 +1,8 @@
## Include this file to indicate that your module does not raise Errors.
## Disables compiler hints about unused declarations in Nim < 1.4.0
when (NimMajor, NimMinor, NimPatch) >= (1, 4, 0):
{.push raises:[].}
else:
{.push raises: [Defect].}
{.hint[XDeclaredButNotUsed]: off.}

View File

@ -6,52 +6,79 @@ import ./chaining
import ./indexing import ./indexing
import ./operators import ./operators
import ./without import ./without
import ./withoutresult
import ./private/bareexcept
include ./errorban include ./private/errorban
export resultsbase except ok, err, isOk, isErr, get export resultsbase except ok, err, isOk, isErr, get
export binding export binding
export chaining export chaining
export indexing export indexing
export without export without
export withoutresult
type ResultFailure* = object of CatchableError type ResultFailure* = object of CatchableError
template `?!`*(T: typed): type Result[T, ref CatchableError] = template `?!`*(T: typed): type Result[T, ref CatchableError] =
## Use `?!` make a Result type. These Result types either hold a value or
## an error. For example the type `?!int` is short for
## `Result[int, ref CatchableError]`.
Result[T, ref CatchableError] Result[T, ref CatchableError]
template `!`*[T](value: ?!T): T = template `!`*[T](value: ?!T): T =
## Returns the value of a Result when you're absolutely sure that it
## contains value. Using `!` on a Result without a value raises a Defect.
value.get value.get
proc success*[T](value: T): ?!T = proc success*[T](value: T): ?!T =
## Creates a successfull Result containing the value.
##
ok(?!T, value) ok(?!T, value)
proc success*: ?!void = proc success*: ?!void =
## Creates a successfull Result without a value.
ok(?!void) ok(?!void)
proc failure*(T: type, error: ref CatchableError): ?!T = proc failure*(T: type, error: ref CatchableError): ?!T =
## Creates a failed Result containing the error.
err(?!T, error) err(?!T, error)
proc failure*(T: type, message: string): ?!T = proc failure*(T: type, message: string): ?!T =
## Creates a failed Result containing a `ResultFailure` with the specified
## error message.
T.failure newException(ResultFailure, message) T.failure newException(ResultFailure, message)
template failure*(error: ref CatchableError): auto = template failure*(error: ref CatchableError): auto =
## Creates a failed Result containing the error.
err error err error
template failure*(message: string): auto = template failure*(message: string): auto =
## Creates a failed Result containing the error.
failure newException(ResultFailure, message) failure newException(ResultFailure, message)
proc isSuccess*[T](value: ?!T): bool = proc isSuccess*[T](value: ?!T): bool =
## Returns true when the Result contains a value.
value.isOk value.isOk
proc isFailure*[T](value: ?!T): bool = proc isFailure*[T](value: ?!T): bool =
## Returns true when the Result contains an error.
value.isErr value.isErr
template `->?`*[T,U](value: ?!T, expression: U): ?!U = proc `$`*[T](value: ?!T): string =
if value.isFailure: if value.isSuccess:
U.failure(value.error) "success(" & $(!value) & ")"
else: else:
expression.success "failure(\"" & $(value.error.msg) & "\")"
template `->?`*[T,U](value: ?!T, expression: ?!U): ?!U = template `->?`*[T,U](value: ?!T, expression: ?!U): ?!U =
if value.isFailure: if value.isFailure:
@ -59,13 +86,8 @@ template `->?`*[T,U](value: ?!T, expression: ?!U): ?!U =
else: else:
expression expression
template `->?`*[T,U,V](values: (?!T, ?!U), expression: V): ?!V = template `->?`*[T,U](value: ?!T, expression: U): ?!U =
if values[0].isFailure: value ->? expression.success
V.failure(values[0].error)
elif values[1].isFailure:
V.failure(values[1].error)
else:
expression.success
template `->?`*[T,U,V](values: (?!T, ?!U), expression: ?!V): ?!V = template `->?`*[T,U,V](values: (?!T, ?!U), expression: ?!V): ?!V =
if values[0].isFailure: if values[0].isFailure:
@ -75,14 +97,51 @@ template `->?`*[T,U,V](values: (?!T, ?!U), expression: ?!V): ?!V =
else: else:
expression expression
template `|?`*[T,E](value: Result[T,E], fallback: T): T = template `->?`*[T,U,V](values: (?!T, ?!U), expression: V): ?!V =
values ->? expression.success
proc `|?`*[T,E](value: Result[T,E], fallback: T): T =
## Use the `|?` operator to supply a fallback value when a Result does not
## hold a value.
value.valueOr(fallback) value.valueOr(fallback)
proc option*[T,E](value: Result[T,E]): ?T = proc option*[T,E](value: Result[T,E]): ?T =
## Converts a Result into an Option.
if value.isOk: if value.isOk:
value.unsafeGet.some ignoreBareExceptWarning:
try: # workaround for erroneous exception tracking when T is a closure
return value.unsafeGet.some
except Exception as exception:
raise newException(Defect, exception.msg, exception)
else: else:
T.none return T.none
template toOption*[T, E](value: Result[T, E]): ?T =
## Converts a Result into an Option.
value.option
proc unsafeCatchableError*[T, E](value: Result[T, E]): ref CatchableError =
## Returns the error from the Result, converted to `ref CatchableError` if
## necessary. Behaviour is undefined when the result holds a value instead of
## an error.
when E is ref CatchableError:
value.unsafeError
else:
when compiles($value.unsafeError):
newException(ResultFailure, $value.unsafeError)
else:
newException(ResultFailure, "Result is an error")
proc errorOption*[T, E](value: Result[T, E]): ?E =
## Returns an Option that contains the error from the Result, if it has one.
if value.isErr:
value.error.some
else:
E.none
Result.liftUnary(`-`) Result.liftUnary(`-`)
Result.liftUnary(`+`) Result.liftUnary(`+`)

View File

@ -1,7 +1,7 @@
template tryImport(module) = import module template tryImport(module) = import module
when compiles tryimport pkg/result: when compiles tryImport pkg/results:
import pkg/result/../results import pkg/results
else: else:
import pkg/stew/results import pkg/stew/results

View File

@ -1,4 +1,6 @@
template without*(expression, body) = template without*(expression, body) =
## Used to place guards that ensure that an Option or Result contains a value.
let ok = expression let ok = expression
if not ok: if not ok:
body body

View File

@ -0,0 +1,41 @@
import std/macros
import ./without
import ./private/binderror
const symbolKinds = {nnkSym, nnkOpenSymChoice, nnkClosedSymChoice}
const identKinds = {nnkIdent} + symbolKinds
proc undoSymbolResolution(expression, ident: NimNode): NimNode =
## Finds symbols in the expression that match the `ident` and replaces them
## with `ident`, effectively undoing any symbol resolution that happened
## before.
if expression.kind in symbolKinds and eqIdent($expression, $ident):
return ident
let expression = expression.copyNimTree()
for i in 0..<expression.len:
expression[i] = undoSymbolResolution(expression[i], ident)
expression
macro without*(condition, errorname, body: untyped): untyped =
## Used to place guards that ensure that a Result contains a value.
## Exposes error when Result does not contain a value.
if errorname.kind notin identKinds:
error("expected an identifier, got " & errorname.repr, errorname)
let errorIdent = ident $errorname
# Nim's early symbol resolution might have picked up a symbol with the
# same name as our error variable. We need to undo this to make sure that our
# error variable is seen.
let body = body.undoSymbolResolution(errorIdent)
quote do:
var error {.gensym.}: ref CatchableError
without captureBindError(error, `condition`):
template `errorIdent`: ref CatchableError = error
`body`

View File

@ -0,0 +1,2 @@
--path:"../.."
import "../../config.nims"

View File

@ -1 +0,0 @@
--path:"../.."

View File

@ -115,17 +115,6 @@ suite "optionals":
else: else:
fail fail
test "=? evaluates optional expression only once":
var count = 0
if a =? (inc count; 42.some):
let b {.used.} = a
check count == 1
count = 0
if var a =? (inc count; 42.some):
let b {.used.} = a
check count == 1
test "=? works in generic code": test "=? works in generic code":
proc toString[T](option: ?T): string = proc toString[T](option: ?T): string =
if value =? option: if value =? option:
@ -148,6 +137,178 @@ suite "optionals":
check 42.some.toString == "42" check 42.some.toString == "42"
check int.none.toString == "none" check int.none.toString == "none"
test "=? works with closures":
var called = false
let closure = some(proc () = called = true)
if a =? none(proc ()):
a()
check not called
if a =? closure:
a()
check called
test "=? works with types that do not have a default value":
type NoDefault {.requiresInit.} = distinct int
proc `==`(a,b: NoDefault): bool {.borrow.}
if a =? some NoDefault(42):
check a == NoDefault(42)
else:
fail()
if var a =? some NoDefault(42):
check a == NoDefault(42)
else:
fail()
test "=? works with reference types":
var x = new int
x[] = 42
if a =? x:
check a[] == 42
else:
fail
x = nil
if a =? x:
fail
var p = proc = discard
if a =? p:
a()
else:
fail
p = nil
if a =? p:
fail
when (NimMajor, NimMinor) >= (1, 1) and (NimMajor, NimMinor) != (2, 0):
var it = iterator: int = yield 2
if a =? it:
for x in a:
check x == 2
else:
fail
it = nil
if a =? it:
fail
test "=? rejects non-reference types":
check `not` compiles do:
if a =? 0:
discard
check `not` compiles do:
if var a =? 0:
discard
check `not` compiles do:
if (a,) =? (0,):
discard
test "=? works with custom optional types":
type MyOption = distinct int
proc isSome(x: MyOption): bool = x.int >= 0
proc unsafeGet(x: MyOption): int = x.int
template toOption(x: MyOption): MyOption = x
if a =? MyOption 42:
check a == 42
else:
fail
if a =? MyOption -1:
fail
test "=? binds and unpacks tuples":
if (a, b) =? (some ("test", 1)):
check a == "test"
check b == 1
else:
fail()
if (a, b) =? none (string, int):
discard a
discard b
fail()
test "=? binds and unpacks tuples with named fields":
if (a, b) =? (some (desc: "test", id: 1)):
check a == "test"
check b == 1
else:
fail()
test "=? binds and unpacks tuples returned from proc":
proc returnsTuple(): ?tuple[name: string, id: int] = some ("test", 1)
if (a, b) =? returnsTuple():
check a == "test"
check b == 1
else:
fail()
test "=? binds and unpacks tuples returned from proc with unnamed fields":
proc returnsTuple(): ?(string, int,) = some ("test", 1,)
if (a, b,) =? returnsTuple():
check a == "test"
check b == 1
else:
fail()
test "=? binds and unpacks tuples with _":
if (_, b) =? some ("test", 1):
check b == 1
else:
fail()
test "=? binds and unpacks tuples with named fields":
if (a, b) =? some (desc: "test", id: 1):
check a == "test"
check b == 1
else:
fail()
test "=? binds variable to tuples with named fields":
if t =? some (desc: "test", id: 1):
check t.desc == "test"
check t.id == 1
else:
fail()
test "=? binds to tuple types":
type MyTuple = tuple
desc: string
id: int
let mt: MyTuple = ("test", 1)
if t =? (some mt):
check t.desc == "test"
check t.id == 1
else:
fail()
if (a, b) =? (some mt):
check a == "test"
check b == 1
else:
fail()
test "=? for tuples does not leak symbols into caller's scope":
const evaluated = ""
type T = string
if (a,) =? some (0,):
check a == 0
check option is proc
check evaluated is string
check T is string
test "without statement can be used for early returns": test "without statement can be used for early returns":
proc test1 = proc test1 =
without a =? 42.some: without a =? 42.some:
@ -174,6 +335,31 @@ suite "optionals":
check table.?["a"] == 1.some check table.?["a"] == 1.some
check table.?["c"] == int.none check table.?["c"] == int.none
test ".?[] can be used for indexing strings without raising IndexDefect":
let str = "a"
check str.?[0] == 'a'.some
check str.?[1] == char.none
check str.?[-1] == char.none
test ".?[] can be used for indexing sequences without raising IndexDefect":
let sequence = @[1]
check sequence.?[0] == 1.some
check sequence.?[1] == int.none
check sequence.?[-1] == int.none
test ".?[] can be used for indexing openArrays without raising IndexDefect":
proc checkOpenArray(oa: openArray[int]): void =
check oa.?[0] == 1.some
check oa.?[1] == int.none
check oa.?[-1] == int.none
checkOpenArray(@[1])
test ".?[] evaluates openArray expression only once":
var count = 0
discard (inc count; @[1].toOpenArray(0, 0)).?[0]
check count == 1
test ".?[] can be followed by calls, operators and indexing": test ".?[] can be followed by calls, operators and indexing":
let table = @{"a": @[41, 42]}.toTable let table = @{"a": @[41, 42]}.toTable
check table.?["a"].isSome check table.?["a"].isSome
@ -246,6 +432,58 @@ suite "optionals":
check a.?[1] == 42.some check a.?[1] == 42.some
test ".? chain evaluates optional expression only once":
var count = 0
discard (inc count; @[41, 42].some).?len
check count == 1
test "=? evaluates optional expression only once":
var count = 0
if a =? (inc count; 42.some):
let b {.used.} = a
check count == 1
count = 0
if var a =? (inc count; 42.some):
let b {.used.} = a
check count == 1
test "|? evaluates optional expression only once":
var count = 0
discard (inc count; 42.some) |? 43
check count == 1
test ".?[] evaluates optional expression only once":
# indexing on optional sequence:
block:
var count = 0
discard (inc count; @[41, 42].some).?[0]
check count == 1
# indexing on normal sequence:
block:
var count = 0
discard (inc count; @[41, 42]).?[0]
check count == 1
test "lifted unary operators evaluate optional expression only once":
var count = 0
discard -(inc count; 42.some)
check count == 1
test "lifted binary operators evaluate optional expressions only once":
# lifted operator on two options:
block:
var count1, count2 = 0
discard (inc count1; 40.some) + (inc count2; 2.some)
check count1 == 1
check count2 == 1
# lifted operator on option and value:
block:
var count1, count2 = 0
discard (inc count1; 40.some) + (inc count2; 2)
check count1 == 1
check count2 == 1
test "examples from readme work": test "examples from readme work":
var x: ?int var x: ?int

View File

@ -4,4 +4,8 @@ description = "Questionable tests for std/option"
license = "MIT" license = "MIT"
task test, "Runs the test suite": task test, "Runs the test suite":
exec "nim c -f -r test.nim" var options = "-f -r --skipParentCfg"
when (NimMajor, NimMinor) >= (1, 4):
options &= " --warningAsError[UnsafeDefault]:on"
options &= " --warningAsError[ProveInit]:on"
exec "nim c " & options & " test.nim"

View File

@ -1 +0,0 @@
--path:"../.."

View File

@ -1,291 +0,0 @@
import std/unittest
import std/options
import std/sequtils
import std/strutils
import std/sugar
import pkg/questionable/results
suite "result":
let error = newException(CatchableError, "error")
test "?!Type is shorthand for Result[Type, ref CatchableError]":
check (?!int is Result[int, ref CatchableError])
check (?!string is Result[string, ref CatchableError])
check (?!seq[bool] is Result[seq[bool], ref CatchableError])
test "! gets value or raises Defect":
check !42.success == 42
expect Defect: discard !int.failure error
test ".? can be used for chaining results":
let a: ?!seq[int] = @[41, 42].success
let b: ?!seq[int] = seq[int].failure error
check a.?len == 2.success
check b.?len == int.failure error
check a.?len.?uint8 == 2'u8.success
check b.?len.?uint8 == uint8.failure error
check a.?len() == 2.success
check b.?len() == int.failure error
check a.?distribute(2).?len() == 2.success
check b.?distribute(2).?len() == int.failure error
test ".? chain can be followed by . calls and operators":
let a = @[41, 42].success
check (a.?len.unsafeGet == 2)
check (a.?len.unsafeGet.uint8.uint64 == 2'u64)
check (a.?len.unsafeGet() == 2)
check (a.?len.unsafeGet().uint8.uint64 == 2'u64)
check (a.?deduplicate()[0].?uint8.?uint64 == 41'u64.success)
check (a.?len + 1 == 3.success)
check (a.?deduplicate()[0] + 1 == 42.success)
check (a.?deduplicate.map(x => x) == @[41, 42].success)
test ".? chains work in generic code":
proc test[T](a: ?!T) =
check (a.?len == 2.success)
check (a.?len.?uint8 == 2'u8.success)
check (a.?len() == 2.success)
check (a.?distribute(2).?len() == 2.success)
check (a.?len.unsafeGet == 2)
check (a.?len.unsafeGet.uint8.uint64 == 2'u64)
check (a.?len.unsafeGet() == 2)
check (a.?len.unsafeGet().uint8.uint64 == 2'u64)
check (a.?deduplicate()[0].?uint8.?uint64 == 41'u64.success)
check (a.?len + 1 == 3.success)
check (a.?deduplicate()[0] + 1 == 42.success)
check (a.?deduplicate.map(x => x) == @[41, 42].success)
test @[41, 42].success
test "[] can be used for indexing results":
let a: ?!seq[int] = @[1, 2, 3].success
let b: ?!seq[int] = seq[int].failure error
check a[1] == 2.success
check a[^1] == 3.success
check a[0..1] == @[1, 2].success
check b[1] == int.failure error
test "|? can be used to specify a fallback value":
check 42.success |? 40 == 42
check int.failure(error) |? 42 == 42
test "=? can be used for optional binding":
if a =? int.failure(error):
fail
if b =? 42.success:
check b == 42
else:
fail
while a =? 42.success:
check a == 42
break
while a =? int.failure(error):
fail
break
test "=? can appear multiple times in conditional expression":
if a =? 42.success and b =? "foo".success:
check a == 42
check b == "foo"
else:
fail
test "=? works with variable hiding":
let a = 42.success
if a =? a:
check a == 42
test "=? works with var":
if var a =? 1.success and var b =? 2.success:
check a == 1
inc a
check a == b
inc b
check b == 3
else:
fail
if var a =? int.failure(error):
fail
test "=? works with .?":
if a =? 42.success.?uint8:
check a == 42.uint8
else:
fail
test "=? evaluates optional expression only once":
var count = 0
if a =? (inc count; 42.success):
let b {.used.} = a
check count == 1
count = 0
if var a =? (inc count; 42.success):
let b {.used.} = a
check count == 1
test "=? works in generic code":
proc toString[T](res: ?!T): string =
if value =? res:
$value
else:
"error"
check 42.success.toString == "42"
check int.failure(error).toString == "error"
test "=? works in generic code with variable hiding":
let value {.used.} = "ignored"
proc toString[T](res: ?!T): string =
if value =? res:
$value
else:
"error"
check 42.success.toString == "42"
check int.failure(error).toString == "error"
test "without statement works for results":
proc test1 =
without a =? 42.success:
fail
return
check a == 42
proc test2 =
without a =? int.failure "error":
return
fail
test1()
test2()
test "catch can be used to convert exceptions to results":
check parseInt("42").catch == 42.success
check parseInt("foo").catch.error of ValueError
test "success can be called without argument":
check (success() is ?!void)
test "failure can be called with string argument":
let value = int.failure("some failure")
check value.error of ResultFailure
check value.error.msg == "some failure"
test "unary operator `-` works for results":
check -(-42.success) == 42.success
check -(int.failure(error)) == int.failure(error)
test "other unary operators work for results":
check +(42.success) == 42.success
check @([1, 2].success) == (@[1, 2]).success
test "binary operator `+` works for results":
check 40.success + 2.success == 42.success
check 40.success + 2 == 42.success
check int.failure(error) + 2 == int.failure(error)
check 40.success + int.failure(error) == int.failure(error)
check int.failure(error) + int.failure(error) == int.failure(error)
test "other binary operators work for results":
check (21.success * 2 == 42.success)
check (84'f.success / 2'f == 42'f.success)
check (84.success div 2 == 42.success)
check (85.success mod 43 == 42.success)
check (0b00110011.success shl 1 == 0b01100110.success)
check (0b00110011.success shr 1 == 0b00011001.success)
check (44.success - 2 == 42.success)
check ("f".success & "oo" == "foo".success)
check (40.success <= 42 == true.success)
check (40.success < 42 == true.success)
check (40.success >= 42 == false.success)
check (40.success > 42 == false.success)
test "Result can be converted to Option":
check 42.success.option == 42.some
check int.failure(error).option == int.none
test "failure can be used without type parameter in procs":
proc fails: ?!int =
failure "some error"
check fails().isFailure
check fails().error.msg == "some error"
test ".? avoids wrapping result in result":
let a = 41.success
proc b(x: int): ?!int =
success x + 1
check a.?b == 42.success
test "lifted operators avoid wrapping result in result":
let a = 40.success
let b = 2.success
func `&`(x, y: int): ?!int =
success x + y
check (a & b) == 42.success
test "examples from readme work":
proc works: ?!seq[int] =
success @[1, 1, 2, 2, 2]
proc fails: ?!seq[int] =
failure "something went wrong"
# binding:
if x =? works():
check x == @[1, 1, 2, 2, 2]
else:
fail
# chaining:
let amount = works().?deduplicate.?len
check (amount == 2.success)
# fallback values:
let value = fails() |? @[]
check (value == newSeq[int](0))
# lifted operators:
let sum = works()[3] + 40
check (sum == 42.success)
# catch
let x = parseInt("42").catch
check (x == 42.success)
let y = parseInt("XX").catch
check y.isFailure
# Conversion to Option
let converted = works().option
check (converted == @[1, 1, 2, 2, 2].some)
import pkg/questionable/resultsbase
suite "result compatibility":
test "|?, =? and .option work on other types of Result":
type R = Result[int, string]
let good = R.ok 42
let bad = R.err "error"
check bad |? 43 == 43
if value =? good:
check value == 42
else:
fail
check good.option == 42.some

View File

@ -0,0 +1,3 @@
--path:"../.."
--threads:on
import "../../config.nims"

View File

@ -0,0 +1,693 @@
import std/unittest
import std/options
import std/sequtils
import std/strutils
import std/sugar
import std/threadpool
import pkg/questionable/results
{.experimental: "parallel".}
suite "result":
let error = newException(CatchableError, "error")
test "?!Type is shorthand for Result[Type, ref CatchableError]":
check (?!int is Result[int, ref CatchableError])
check (?!string is Result[string, ref CatchableError])
check (?!seq[bool] is Result[seq[bool], ref CatchableError])
test "conversion to string $ works for ?!Types":
check $42.success == "success(42)"
check $(int.failure "some error") == "failure(\"some error\")"
test "! gets value or raises Defect":
check !42.success == 42
expect Defect: discard !int.failure error
test ".? can be used for chaining results":
let a: ?!seq[int] = @[41, 42].success
let b: ?!seq[int] = seq[int].failure error
check a.?len == 2.success
check b.?len == int.failure error
check a.?len.?uint8 == 2'u8.success
check b.?len.?uint8 == uint8.failure error
check a.?len() == 2.success
check b.?len() == int.failure error
check a.?distribute(2).?len() == 2.success
check b.?distribute(2).?len() == int.failure error
test ".? chain can be followed by . calls and operators":
let a = @[41, 42].success
check (a.?len.unsafeGet == 2)
check (a.?len.unsafeGet.uint8.uint64 == 2'u64)
check (a.?len.unsafeGet() == 2)
check (a.?len.unsafeGet().uint8.uint64 == 2'u64)
check (a.?deduplicate()[0].?uint8.?uint64 == 41'u64.success)
check (a.?len + 1 == 3.success)
check (a.?deduplicate()[0] + 1 == 42.success)
check (a.?deduplicate.map(x => x) == @[41, 42].success)
test ".? chains work in generic code":
proc test[T](a: ?!T) =
check (a.?len == 2.success)
check (a.?len.?uint8 == 2'u8.success)
check (a.?len() == 2.success)
check (a.?distribute(2).?len() == 2.success)
check (a.?len.unsafeGet == 2)
check (a.?len.unsafeGet.uint8.uint64 == 2'u64)
check (a.?len.unsafeGet() == 2)
check (a.?len.unsafeGet().uint8.uint64 == 2'u64)
check (a.?deduplicate()[0].?uint8.?uint64 == 41'u64.success)
check (a.?len + 1 == 3.success)
check (a.?deduplicate()[0] + 1 == 42.success)
check (a.?deduplicate.map(x => x) == @[41, 42].success)
test @[41, 42].success
test "[] can be used for indexing results":
let a: ?!seq[int] = @[1, 2, 3].success
let b: ?!seq[int] = seq[int].failure error
check a[1] == 2.success
check a[^1] == 3.success
check a[0..1] == @[1, 2].success
check b[1] == int.failure error
test "|? can be used to specify a fallback value":
check 42.success |? 40 == 42
check int.failure(error) |? 42 == 42
test "=? can be used for optional binding":
if a =? int.failure(error):
fail
if b =? 42.success:
check b == 42
else:
fail
while a =? 42.success:
check a == 42
break
while a =? int.failure(error):
fail
break
test "=? can appear multiple times in conditional expression":
if a =? 42.success and b =? "foo".success:
check a == 42
check b == "foo"
else:
fail
test "=? works with variable hiding":
let a = 42.success
if a =? a:
check a == 42
test "=? works with var":
if var a =? 1.success and var b =? 2.success:
check a == 1
inc a
check a == b
inc b
check b == 3
else:
fail
if var a =? int.failure(error):
fail
test "=? works with .?":
if a =? 42.success.?uint8:
check a == 42.uint8
else:
fail
test "=? evaluates optional expression only once":
var count = 0
if a =? (inc count; 42.success):
let b {.used.} = a
check count == 1
count = 0
if var a =? (inc count; 42.success):
let b {.used.} = a
check count == 1
test "=? works in generic code":
proc toString[T](res: ?!T): string =
if value =? res:
$value
else:
"error"
check 42.success.toString == "42"
check int.failure(error).toString == "error"
test "=? works in generic code with variable hiding":
let value {.used.} = "ignored"
proc toString[T](res: ?!T): string =
if value =? res:
$value
else:
"error"
check 42.success.toString == "42"
check int.failure(error).toString == "error"
test "=? works with closures":
var called = false
let closure = success(proc () = called = true)
if a =? failure(proc (), error):
a()
check not called
if a =? closure:
a()
check called
test "=? binds and unpacks tuples":
if (a, b) =? (success ("test", 1)):
check a == "test"
check b == 1
else:
fail()
if (a, b) =? (string, int).failure(error):
discard a
discard b
fail()
test "=? binds and unpacks tuples with named fields":
if (a, b) =? (success (desc: "test", id: 1)):
check a == "test"
check b == 1
else:
fail()
test "=? binds and unpacks tuples returned from proc":
proc returnsTuple(): ?!tuple[name: string, id: int] = success ("test", 1)
if (a, b) =? returnsTuple():
check a == "test"
check b == 1
else:
fail()
test "=? binds and unpacks tuples returned from proc with unnamed fields":
proc returnsTuple(): ?!(string, int,) = success ("test", 1,)
if (a, b,) =? returnsTuple():
check a == "test"
check b == 1
else:
fail()
test "=? binds and unpacks tuples with _":
if (_, b) =? success ("test", 1):
check b == 1
else:
fail()
test "=? binds and unpacks tuples with named fields":
if (a, b) =? success (desc: "test", id: 1):
check a == "test"
check b == 1
else:
fail()
test "=? binds variable to tuples with named fields":
if t =? success (desc: "test", id: 1):
check t.desc == "test"
check t.id == 1
else:
fail()
test "=? binds to tuple types":
type MyTuple = tuple
desc: string
id: int
let mt: MyTuple = ("test", 1)
if t =? (success mt):
check t.desc == "test"
check t.id == 1
else:
fail()
if (a, b) =? (success mt):
check a == "test"
check b == 1
else:
fail()
test "without statement works for results":
proc test1 =
without a =? 42.success:
fail
return
check a == 42
proc test2 =
without a =? int.failure "error":
return
fail
test1()
test2()
test "without statement can expose error":
proc test =
without a =? int.failure "some error", error:
check error.msg == "some error"
return
fail
test()
test "without statement only exposes error variable inside block":
proc test =
without a =? 42.success, errorvar:
fail
discard errorvar # fixes warning about unused variable "errorvar"
return
check not compiles errorvar
test()
test "without statements with multiple bindings exposes first error":
proc test1 =
without (a =? int.failure "error 1") and
(b =? int.failure "error 2"),
error:
check error.msg == "error 1"
return
fail
proc test2 =
without (a =? 42.success) and (b =? int.failure "error 2"), error:
check error.msg == "error 2"
return
fail
test1()
test2()
test "without statement with error evaluates result only once":
proc test =
var count = 0
without a =? (inc count; int.failure "error"):
check count == 1
return
fail
test()
test "without statement with error handles options as well":
proc test1 =
without a =? int.none and b =? int.failure "error", error:
check error.msg == "Option is set to `none`"
return
fail
proc test2 =
without a =? 42.some and b =? int.failure "error", error:
check error.msg == "error"
return
fail
test1()
test2()
test "without statement with error handles references as well":
proc test =
var x: ref int = nil
without a =? x, error:
check error.msg == "ref is nil"
return
fail
test()
test "without statement with error handles pointers as well":
proc test =
var x: ptr int = nil
without a =? x, error:
check error.msg == "ptr is nil"
return
fail
test()
test "without statement with error handles closures as well":
proc test =
var x = proc = discard
x = nil
without a =? x, error:
check error.msg == "proc or iterator is nil"
return
fail
test()
test "without statement with error handles iterators as well":
when (NimMajor, NimMinor) != (2, 0):
proc test =
var x: iterator: int = nil
without a =? x, error:
check error.msg == "proc or iterator is nil"
return
fail
test()
test "without statement with error can be used more than once":
proc test =
without a =? 42.success, error:
discard error
fail
without b =? 42.success, error:
discard error
fail
test()
test "without statement with error works with deeply nested =? operators":
proc test =
let fail1 = int.failure "error 1"
let fail2 = int.failure "error 2"
without (block: a =? (if b =? fail1: b.success else: fail2)), error:
check error.msg == "error 2"
return
fail
test()
test "without statement with error works in generic code":
proc test(_: type) =
without a =? int.failure "error", e:
check e.msg == "error"
return
fail
test(int)
test "without statements with error can be nested":
without a =? int.failure "error1", e1:
without b =? int.failure "error2", e2:
check e1.msg == "error1"
check e2.msg == "error2"
check e1.msg == "error1"
test "without statement works in generic code using existing error name":
let existingName {.used.} = "some variable"
proc shouldCompile(_: type int): ?!int =
without _ =? int.failure "error", existingName:
check existingName.msg == "error"
return success 42
discard int.shouldCompile()
test "without statements with error work in nested calls":
proc bar(): ?!int =
without _ =? int.failure "error", err:
return failure err.msg
proc foo() =
without _ =? bar(), err:
check err.msg == "error"
return
fail()
foo()
test "without statement with error works in nested generic calls":
proc works(_: type int): ?!int =
without _ =? int.failure "error1", error:
check error.msg == "error1"
return success 42
proc fails(_: type int): ?!int =
return failure "error2"
proc foo =
without a =? int.works() and b =? int.fails(), error:
check error.msg == "error2"
return
fail()
foo()
test "without statement with error works with multiple threads":
proc fail(number: int) =
without _ =? int.failure "error" & $number, error:
check error.msg == "error" & $number
parallel:
for i in 0..<1000:
spawn fail(i)
test "without statement doesn't interfere with generic code called elsewhere":
proc foo(_: type): ?!int =
if error =? success(1).errorOption:
discard
proc bar {.used.} = # defined, but not used
without x =? bool.foo(), error:
discard error
discard bool.foo() # same type parameter 'bool' as used in bar()
test "catch can be used to convert exceptions to results":
check parseInt("42").catch == 42.success
check parseInt("foo").catch.error of ValueError
test "success can be called without argument":
check (success() is ?!void)
test "failure can be called with string argument":
let value = int.failure("some failure")
check value.error of ResultFailure
check value.error.msg == "some failure"
test "unary operator `-` works for results":
check -(-42.success) == 42.success
check -(int.failure(error)) == int.failure(error)
test "other unary operators work for results":
check +(42.success) == 42.success
check @([1, 2].success) == (@[1, 2]).success
test "binary operator `+` works for results":
check 40.success + 2.success == 42.success
check 40.success + 2 == 42.success
check int.failure(error) + 2 == int.failure(error)
check 40.success + int.failure(error) == int.failure(error)
check int.failure(error) + int.failure(error) == int.failure(error)
test "other binary operators work for results":
check (21.success * 2 == 42.success)
check (84'f.success / 2'f == 42'f.success)
check (84.success div 2 == 42.success)
check (85.success mod 43 == 42.success)
check (0b00110011.success shl 1 == 0b01100110.success)
check (0b00110011.success shr 1 == 0b00011001.success)
check (44.success - 2 == 42.success)
check ("f".success & "oo" == "foo".success)
check (40.success <= 42 == true.success)
check (40.success < 42 == true.success)
check (40.success >= 42 == false.success)
check (40.success > 42 == false.success)
test "Result can be converted to Option":
check 42.success.option == 42.some
check int.failure(error).option == int.none
test "Result error can be converted to Option":
check (int.failure(error).errorOption == error.some)
check (42.success.errorOption == (ref CatchableError).none)
check (void.failure(error).errorOption == error.some)
check (success().errorOption == (ref CatchableError).none)
test "failure can be used without type parameter in procs":
proc fails: ?!int =
failure "some error"
check fails().isFailure
check fails().error.msg == "some error"
test ".? avoids wrapping result in result":
let a = 41.success
proc b(x: int): ?!int =
success x + 1
check a.?b == 42.success
test "lifted operators avoid wrapping result in result":
let a = 40.success
let b = 2.success
func `&`(x, y: int): ?!int =
success x + y
check (a & b) == 42.success
test ".? chain evaluates result only once":
var count = 0
discard (inc count; @[41, 42].success).?len
check count == 1
test "=? evaluates result only once":
var count = 0
if a =? (inc count; 42.success):
let b {.used.} = a
check count == 1
count = 0
if var a =? (inc count; 42.success):
let b {.used.} = a
check count == 1
test "|? evaluates result only once":
var count = 0
discard (inc count; 42.success) |? 43
check count == 1
test ".?[] evaluates result only once":
var count = 0
discard (inc count; @[41, 42].success).?[0]
check count == 1
test "lifted unary operators evaluate result only once":
var count = 0
discard -(inc count; 42.success)
check count == 1
test "lifted binary operators evaluate results only once":
# lifted operator on two options:
block:
var count1, count2 = 0
discard (inc count1; 40.success) + (inc count2; 2.success)
check count1 == 1
check count2 == 1
# lifted operator on option and value:
block:
var count1, count2 = 0
discard (inc count1; 40.success) + (inc count2; 2)
check count1 == 1
check count2 == 1
test "conversion to option evaluates result only once":
var count = 0
discard (inc count; 42.success).option
check count == 1
test "conversion to error evaluates result only once":
var count = 0
discard (inc count; int.failure(error)).errorOption
check count == 1
test "examples from readme work":
proc works: ?!seq[int] =
success @[1, 1, 2, 2, 2]
proc fails: ?!seq[int] =
failure "something went wrong"
# binding:
if x =? works():
check x == @[1, 1, 2, 2, 2]
else:
fail
# chaining:
let amount = works().?deduplicate.?len
check (amount == 2.success)
# fallback values:
let value = fails() |? @[]
check (value == newSeq[int](0))
# lifted operators:
let sum = works()[3] + 40
check (sum == 42.success)
# catch
let x = parseInt("42").catch
check (x == 42.success)
let y = parseInt("XX").catch
check y.isFailure
# Conversion to Option
let converted = works().option
check (converted == @[1, 1, 2, 2, 2].some)
# Without statement
proc someProc(r: ?!int) =
without value =? r, error:
check error.msg == "some error"
return
check value == 42
someProc(42.success)
someProc(int.failure "some error")
type TypeWithSideEffect = object
proc `$`*(value: TypeWithSideEffect): string {.sideEffect.} =
discard
suite "result side effects":
test "without statement with error works when `$` has side effects":
proc foo =
without x =? TypeWithSideEffect.failure("error"), error:
discard error
return
fail()
foo()
import pkg/questionable/resultsbase
suite "result compatibility":
type R = Result[int, string]
let good = R.ok 42
let bad = R.err "some error"
test "|?, =? and .option work on other types of Result":
check bad |? 43 == 43
if value =? good:
check value == 42
else:
fail
check good.option == 42.some
test "=? works on other type of Result after without statement with error":
without a =? 42.success, error:
discard error # fixes warning about unused variable "error"
fail
without b =? good:
fail
test "without statement with error works on other type of Result":
without value =? bad, error:
check error of ResultFailure
check error.msg == "some error"
test "without statement with error works on Result[T, void]":
without value =? Result[int, void].err, error:
check error of ResultFailure
check error.msg == "Result is an error"

View File

@ -3,7 +3,7 @@ author = "Questionable Authors"
description = "Questionable tests for pkg/result" description = "Questionable tests for pkg/result"
license = "MIT" license = "MIT"
requires "result" requires "results"
task test, "Runs the test suite": task test, "Runs the test suite":
exec "nim c -f -r test.nim" exec "nim c -f -r --skipParentCfg test.nim"

View File

@ -0,0 +1,3 @@
--path:"../.."
--threads:on
import "../../config.nims"

View File

@ -1 +0,0 @@
--path:"../.."

View File

@ -1 +1 @@
include ../result/test include ../results/test

View File

@ -3,7 +3,11 @@ author = "Questionable Authors"
description = "Questionable tests for pkg/stew" description = "Questionable tests for pkg/stew"
license = "MIT" license = "MIT"
requires "stew" when (NimMajor, NimMinor) >= (1, 6):
requires "stew"
task test, "Runs the test suite": task test, "Runs the test suite":
exec "nim c -f -r test.nim" exec "nim c -f -r --skipParentCfg test.nim"
else:
task test, "Runs the test suite":
echo "Warning: Skipping test with stew on Nim < 1.6"