Compare commits

...

19 Commits

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
10 changed files with 278 additions and 45 deletions

View File

@ -12,7 +12,7 @@ Use the [Nimble][3] package manager to add `questionable` to an existing
project. Add the following to its .nimble file:
```nim
requires "questionable >= 0.10.11 & < 0.11.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
@ -151,7 +151,7 @@ have to explicitly import the `questionable/results` module:
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
CatchableError]`.
@ -226,6 +226,7 @@ Any Result can be converted to an Option:
```nim
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

View File

@ -1,4 +1,4 @@
version = "0.10.11"
version = "0.10.15"
author = "Questionable Authors"
description = "Elegant optional types"
license = "MIT"

View File

@ -2,15 +2,25 @@ import std/options
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
template toOption[T: SomePointer](value: T): Option[T] =
value.option
proc placeholder(T: type): T =
discard
template bindLet(name, expression): untyped =
let evaluated = expression
let option = evaluated.option
let option = evaluated.toOption
type T = typeof(option.unsafeGet())
let name {.used.} = if option.isSome:
option.unsafeGet()
@ -21,7 +31,7 @@ template bindLet(name, expression): untyped =
template bindVar(name, expression): untyped =
let evaluated = expression
let option = evaluated.option
let option = evaluated.toOption
type T = typeof(option.unsafeGet())
var name {.used.} = if option.isSome:
option.unsafeGet()
@ -40,9 +50,9 @@ proc newUnpackTupleNode(names: NimNode, value: NimNode): NimNode =
nnkLetSection.newTree(vartuple)
macro bindTuple(names, expression): bool =
let opt = ident("option")
let evaluated = ident("evaluated")
let T = ident("T")
let opt = genSym(nskLet, "option")
let evaluated = genSym(nskLet, "evaluated")
let T = genSym(nskType, "T")
let value = quote do:
if `opt`.isSome:
@ -55,7 +65,7 @@ macro bindTuple(names, expression): bool =
quote do:
let `evaluated` = `expression`
let `opt` = `evaluated`.option
let `opt` = `evaluated`.toOption
type `T` = typeof(`opt`.unsafeGet())
`letsection`
`opt`.isSome

View File

@ -1,16 +1,24 @@
import std/macros
import std/options
macro `.?`*(expression: seq | string, brackets: untyped{nkBracket}): untyped =
# chain is of shape: (seq or string).?[index]
let index = brackets[0]
quote do:
block:
type T = typeof(`expression`[`index`])
let evaluated = `expression`
if `index` < evaluated.len:
evaluated[`index`].some
else:
T.none
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 =
# chain is of shape: expression.?[index]

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

@ -1,24 +1,54 @@
import std/options
import std/macros
var captures {.global, compileTime.}: int
var errorVariable {.threadvar.}: ptr ref CatchableError
# A stack of names of error variables. Keeps track of the error variables that
# are given to captureBindError().
var errorVariableNames {.global, compileTime.}: seq[string]
template captureBindError*(error: var ref CatchableError, expression): auto =
let previousErrorVariable = errorVariable
errorVariable = addr error
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.
static: inc captures
let evaluated = expression
static: dec captures
# name of the error variable as a string literal
let errorVariableName = newLit($error)
errorVariable = previousErrorVariable
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`
evaluated
func error[T](option: Option[T]): ref CatchableError =
func unsafeCatchableError[T](_: Option[T]): ref CatchableError =
newException(ValueError, "Option is set to `none`")
template bindFailed*(expression) =
when captures > 0:
mixin error
errorVariable[] = expression.error
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

@ -7,6 +7,7 @@ import ./indexing
import ./operators
import ./without
import ./withoutresult
import ./private/bareexcept
include ./private/errorban
@ -109,12 +110,30 @@ proc option*[T,E](value: Result[T,E]): ?T =
## Converts a Result into an Option.
if value.isOk:
try: # workaround for erroneous exception tracking when T is a closure
value.unsafeGet.some
except Exception as exception:
raise newException(Defect, exception.msg, exception)
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:
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.

View File

@ -34,7 +34,7 @@ macro without*(condition, errorname, body: untyped): untyped =
let body = body.undoSymbolResolution(errorIdent)
quote do:
var error: ref CatchableError
var error {.gensym.}: ref CatchableError
without captureBindError(error, `condition`):
template `errorIdent`: ref CatchableError = error

View File

@ -165,6 +165,65 @@ suite "optionals":
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"
@ -241,6 +300,15 @@ suite "optionals":
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":
proc test1 =
without a =? 42.some:
@ -269,13 +337,28 @@ suite "optionals":
test ".?[] can be used for indexing strings without raising IndexDefect":
let str = "a"
check str.?[0] == 'a'.some
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":
let table = @{"a": @[41, 42]}.toTable

View File

@ -326,6 +326,48 @@ suite "result":
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:
@ -412,6 +454,17 @@ suite "result":
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
@ -590,6 +643,19 @@ suite "result":
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
@ -597,7 +663,7 @@ suite "result compatibility":
type R = Result[int, string]
let good = R.ok 42
let bad = R.err "error"
let bad = R.err "some error"
test "|?, =? and .option work on other types of Result":
check bad |? 43 == 43
@ -615,3 +681,13 @@ suite "result compatibility":
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"