nim-stew/stew/results.nim

671 lines
24 KiB
Nim

# nim-result is also available stand-alone from https://github.com/arnetheduck/nim-result/
# Copyright (c) 2019 Jacek Sieka
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.
type
ResultError*[E] = object of ValueError
## Error raised when using `tryGet` value of result when error is set
## See also Exception bridge mode
error*: E
ResultDefect* = object of Defect
## Defect raised when accessing value when error is set and vice versa
## See also Exception bridge mode
Result*[T, E] = object
## Result type that can hold either a value or an error, but not both
##
## # Example
##
## ```
## # It's convenient to create an alias - most likely, you'll do just fine
## # with strings or cstrings as error
##
## type R = Result[int, string]
##
## # Once you have a type, use `ok` and `err`:
##
## func works(): R =
## # ok says it went... ok!
## R.ok 42
## func fails(): R =
## # or type it like this, to not repeat the type!
## result.err "bad luck"
##
## if (let w = works(); w.isOk):
## echo w[], " or use value: ", w.value
##
## # In case you think your callers want to differentiate between errors:
## type
## Error = enum
## a, b, c
## type RE[T] = Result[T, Error]
##
## # In the expriments corner, you'll find the following syntax for passing
## # errors up the stack:
## func f(): R =
## let x = ?works() - ?fails()
## assert false, "will never reach"
##
## # If you provide this exception converter, this exception will be raised
## # on dereference
## func toException(v: Error): ref CatchableError = (ref CatchableError)(msg: $v)
## try:
## RE[int].err(a)[]
## except CatchableError:
## echo "in here!"
##
## ```
##
## See the tests for more practical examples, specially when working with
## back and forth with the exception world!
##
## # Potential benefits:
##
## * Handling errors becomes explicit and mandatory at the call site -
## goodbye "out of sight, out of mind"
## * Errors are a visible part of the API - when they change, so must the
## calling code and compiler will point this out - nice!
## * Errors are a visible part of the API - your fellow programmer is
## reminded that things actually can go wrong
## * Jives well with Nim `discard`
## * Jives well with the new Defect exception hierarchy, where defects
## are raised for unrecoverable errors and the rest of the API uses
## results
## * Error and value return have similar performance characteristics
## * Caller can choose to turn them into exceptions at low cost - flexible
## for libraries!
## * Mostly relies on simple Nim features - though this library is no
## exception in that compiler bugs were discovered writing it :)
##
## # Potential costs:
##
## * Handling errors becomes explicit and mandatory - if you'd rather ignore
## them or just pass them to some catch-all, this is noise
## * When composing operations, value must be lifted before processing,
## adding potential verbosity / noise (fancy macro, anyone?)
## * There's no call stack captured by default (see also `catch` and
## `capture`)
## * The extra branching may be more expensive for the non-error path
## (though this can be minimized with PGO)
##
## The API visibility issue of exceptions can also be solved with
## `{.raises.}` annotations - as of now, the compiler doesn't remind
## you to do so, even though it knows what the right annotation should be.
## `{.raises.}` does not participate in generic typing, making it just as
## verbose but less flexible in some ways, if you want to type it out.
##
## Many system languages make a distinction between errors you want to
## handle and those that are simply bugs or unrealistic to deal with..
## handling the latter will often involve aborting or crashing the funcess -
## reliable systems like Erlang will try to relaunch it.
##
## On the flip side we have dynamic languages like python where there's
## nothing exceptional about exceptions (hello StopIterator). Python is
## rarely used to build reliable systems - its strengths lie elsewhere.
##
## # Exception bridge mode
##
## When the error of a `Result` is an `Exception`, or a `toException` helper
## is present for your error type, the "Exception bridge mode" is
## enabled and instead of raising `ResultError`, `tryGet` will raise the
## given `Exception` on access. `[]` and `get` will continue to raise a
## `Defect`.
##
## This is an experimental feature that may be removed.
##
## # Other languages
##
## Result-style error handling seems pretty popular lately, specially with
## statically typed languages:
## Haskell: https://hackage.haskell.org/package/base-4.11.1.0/docs/Data-Either.html
## Rust: https://doc.rust-lang.org/std/result/enum.Result.html
## Modern C++: https://github.com/viboes/std-make/tree/master/doc/proposal/expected
## More C++: https://github.com/ned14/outcome
##
## Swift is interesting in that it uses a non-exception implementation but
## calls errors exceptions and has lots of syntactic sugar to make them feel
## that way by implicitly passing them up the call chain - with a mandatory
## annotation that function may throw:
## https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html
##
## # Considerations for the error type
##
## * Use a `string` or a `cstring` if you want to provide a diagnostic for
## the caller without an expectation that they will differentiate between
## different errors. Callers should never parse the given string!
## * Use an `enum` to provide in-depth errors where the caller is expected
## to have different logic for different errors
## * Use a complex type to include error-specific meta-data - or make the
## meta-data collection a visible part of your API in another way - this
## way it remains discoverable by the caller!
##
## A natural "error API" progression is starting with `Option[T]`, then
## `Result[T, cstring]`, `Result[T, enum]` and `Result[T, object]` in
## escalating order of complexity.
##
## # Other implemenations in nim
##
## There are other implementations in nim that you might prefer:
## * Either from nimfp: https://github.com/vegansk/nimfp/blob/master/src/fp/either.nim
## * result_type: https://github.com/kapralos/result_type/
##
## # Implementation notes
##
## This implementation is mostly based on the one in rust. Compared to it,
## there are a few differences - if know of creative ways to improve things,
## I'm all ears.
##
## * Rust has the enum variants which lend themselves to nice construction
## where the full Result type isn't needed: `Err("some error")` doesn't
## need to know value type - maybe some creative converter or something
## can deal with this?
## * Nim templates allow us to fail fast without extra effort, meaning the
## other side of `and`/`or` isn't evaluated unless necessary - nice!
## * Rust uses From traits to deal with result translation as the result
## travels up the call stack - needs more tinkering - some implicit
## conversions would be nice here
## * Pattern matching in rust allows convenient extraction of value or error
## in one go.
##
## # Performance considerations
##
## When returning a Result instead of a simple value, there are a few things
## to take into consideration - in general, we are returning more
## information directly to the caller which has an associated cost.
##
## Result is a value type, thus its performance characteristics
## generally follow the performance of copying the value or error that
## it stores. `Result` would benefit greatly from "move" support in the
## language.
##
## In many cases, these performance costs are negligeable, but nonetheless
## they are important to be aware of, to structure your code in an efficient
## manner:
##
## * Memory overhead
## Result is stored in memory as a union with a `bool` discriminator -
## alignment makes it somewhat tricky to give an exact size, but in
## general, `Result[int, int]` will take up `2*sizeof(int)` bytes:
## 1 `int` for the discriminator and padding, 1 `int` for either the value
## or the error. The additional size means that returning may take up more
## registers or spill onto the stack.
## * Loss of RVO
## Nim does return-value-optimization by rewriting `proc f(): X` into
## `proc f(result: var X)` - in an expression like `let x = f()`, this
## allows it to avoid a copy from the "temporary" return value to `x` -
## when using Result, this copy currently happens always because you need
## to fetch the value from the Result in a second step: `let x = f().value`
## * Extra copies
## To avoid spurious evaluation of expressions in templates, we use a
## temporary variable sometimes - this means an unnecessary copy for some
## types.
## * Bad codegen
## When doing RVO, Nim generates poor and slow code: it uses a construct
## called `genericReset` that will zero-initialize a value using dynamic
## RTTI - a process that the C compiler subsequently is unable to
## optimize. This applies to all types, but is exacerbated with Result
## because of its bigger footprint - this should be fixed in compiler.
## * Double zero-initialization bug
## Nim has an initialization bug that causes additional poor performance:
## `var x = f()` will be expanded into `var x; zeroInit(x); f(x)` where
## `f(x)` will call the slow `genericReset` and zero-init `x` again,
## unnecessarily.
##
## Comparing `Result` performance to exceptions in Nim is difficult - it
## will depend on the error type used, the frequency at which exceptions
## happen, the amount of error handling code in the application and the
## compiler and backend used.
##
## * the default C backend in nim uses `setjmp` for exception handling -
## the relative performance of the happy path will depend on the structure
## of the code: how many exception handlers there are, how much unwinding
## happens. `setjmp` works by taking a snapshot of the full CPU state and
## saving it to memory when enterting a try block (or an implict try
## block, such as is introduced with `defer` and similar constructs).
## * an efficient exception handling mechanism (like the C++ backend or
## `nlvm`) will usually have a lower cost on the happy path because the
## value can be returned more efficiently. However, there is still a code
## and data size increase depending on the specific situation, as well as
## loss of optimization opportunities to consider.
## * raising an exception is usually (a lot) slower than returning an error
## through a Result - at raise time, capturing a call stack and allocating
## memory for the Exception is expensive, so the performance difference
## comes down to the complexity of the error type used.
## * checking for errors with Result is local branching operation that also
## happens on the happy path - this may be a cost.
##
## An accurate summary might be that Exceptions are at its most efficient
## when errors are not handled and don't happen.
##
## # Relevant nim bugs
##
## https://github.com/nim-lang/Nim/issues/13799 - type issues
## https://github.com/nim-lang/Nim/issues/8745 - genericReset slow
## https://github.com/nim-lang/Nim/issues/13879 - double-zero-init slow
## https://github.com/nim-lang/Nim/issues/14318 - generic error raises pragma
case o: bool
of false:
e: E
of true:
v: T
Opt*[T] = Result[T, void]
func raiseResultError[T, E](self: Result[T, E]) {.noreturn, noinline.} =
# noinline because raising should take as little space as possible at call
# site
mixin toException
when E is ref Exception:
if self.e.isNil: # for example Result.default()!
raise (ref ResultError[void])(msg: "Trying to access value with err (nil)")
raise self.e
elif compiles(toException(self.e)):
raise toException(self.e)
elif compiles($self.e):
raise (ref ResultError[E])(
error: self.e, msg: "Trying to access value with err: " & $self.e)
else:
raise (res ResultError[E])(msg: "Trying to access value with err", error: self.e)
func raiseResultDefect(m: string, v: auto) {.noreturn, noinline.} =
mixin `$`
when compiles($v): raise (ref ResultDefect)(msg: m & ": " & $v)
else: raise (ref ResultDefect)(msg: m)
func raiseResultDefect(m: string) {.noreturn, noinline.} =
raise (ref ResultDefect)(msg: m)
template assertOk(self: Result) =
if not self.o:
when self.E isnot void:
raiseResultDefect("Trying to access value with err Result", self.e)
else:
raiseResultDefect("Trying to access value with err Result")
template ok*[T, E](R: type Result[T, E], x: auto): R =
## Initialize a result with a success and value
## Example: `Result[int, string].ok(42)`
R(o: true, v: x)
template ok*[T, E](self: var Result[T, E], x: auto) =
## Set the result to success and update value
## Example: `result.ok(42)`
self = ok(type self, x)
template err*[T, E](R: type Result[T, E], x: auto): R =
## Initialize the result to an error
## Example: `Result[int, string].err("uh-oh")`
R(o: false, e: x)
template err*[T](R: type Result[T, cstring], x: string): R =
## Initialize the result to an error
## Example: `Result[int, string].err("uh-oh")`
const s = x
R(o: false, e: cstring(s))
template err*[T](R: type Result[T, void]): R =
R(o: false)
template err*[T, E](self: var Result[T, E], x: auto) =
## Set the result as an error
## Example: `result.err("uh-oh")`
self = err(type self, x)
template err*[T](self: var Result[T, cstring], x: string) =
const s = x # Make sure we don't return a dangling pointer
self = err(type self, cstring(s))
template err*[T](self: var Result[T, void]) =
## Set the result as an error
## Example: `result.err()`
self = err(type self)
template ok*(v: auto): auto = ok(typeof(result), v)
template err*(v: auto): auto = err(typeof(result), v)
template isOk*(self: Result): bool = self.o
template isErr*(self: Result): bool = not self.o
template isSome*(o: Opt): bool =
## Alias for `isOk`
isOk o
template isNone*(o: Opt): bool =
## Alias of `isErr`
isErr o
func map*[T, E, A](
self: Result[T, E], f: proc(x: T): A): Result[A, E] {.inline.} =
## Transform value using f, or return error
##
## ```
## let r = Result[int, cstring).ok(42)
## assert r.map(proc (v: int): int = $v).get() == "42"
## ```
if self.o: result.ok(f(self.v))
else: result.err(self.e)
func flatMap*[T, E, A](
self: Result[T, E], f: proc(x: T): Result[A, E]): Result[A, E] {.inline.} =
if self.o: f(self.v)
else: Result[A, E].err(self.e)
func mapErr*[T: not void, E, A](
self: Result[T, E], f: proc(x: E): A): Result[T, A] {.inline.} =
## Transform error using f, or return value
if self.o: result.ok(self.v)
else: result.err(f(self.e))
func mapConvert*[T0, E0](
self: Result[T0, E0], T1: type): Result[T1, E0] {.inline.} =
## Convert result value to A using an conversion
# Would be nice if it was automatic...
if self.o: result.ok(T1(self.v))
else: result.err(self.e)
func mapCast*[T0, E0](
self: Result[T0, E0], T1: type): Result[T1, E0] {.inline.} =
## Convert result value to A using a cast
## Would be nice with nicer syntax...
if self.o: result.ok(cast[T1](self.v))
else: result.err(self.e)
template `and`*[T0, E, T1](self: Result[T0, E], other: Result[T1, E]): Result[T1, E] =
## Evaluate `other` iff self.isOk, else return error
## fail-fast - will not evaluate other if a is an error
let s = self
if s.o:
other
else:
when type(self) is type(other):
s
else:
type R = type(other)
err(R, s.e)
template `or`*[T, E0, E1](self: Result[T, E0], other: Result[T, E1]): Result[T, E1] =
## Evaluate `other` iff `not self.isOk`, else return `self`
## fail-fast - will not evaluate `other` if `self` is ok
##
## ```
## func f(): Result[int, SomeEnum] =
## f2() or err(EnumValue) # Collapse errors from other module / function
## ```
let s = self
if s.o:
when type(self) is type(other):
s
else:
type R = type(other)
ok(R, s.v)
else:
other
template catch*(body: typed): Result[type(body), ref CatchableError] =
## Catch exceptions for body and store them in the Result
##
## ```
## let r = catch: someFuncThatMayRaise()
## ```
type R = Result[type(body), ref CatchableError]
try:
R.ok(body)
except CatchableError as e:
R.err(e)
template capture*[E: Exception](T: type, someExceptionExpr: ref E): Result[T, ref E] =
## Evaluate someExceptionExpr and put the exception into a result, making sure
## to capture a call stack at the capture site:
##
## ```
## let e: Result[void, ValueError] = void.capture((ref ValueError)(msg: "test"))
## echo e.error().getStackTrace()
## ```
type R = Result[T, ref E]
var ret: R
try:
# TODO is this needed? I think so, in order to grab a call stack, but
# haven't actually tested...
if true:
# I'm sure there's a nicer way - this just works :)
raise someExceptionExpr
except E as caught:
ret = R.err(caught)
ret
func `==`*[T0: not void, E0, T1: not void, E1](lhs: Result[T0, E0], rhs: Result[T1, E1]): bool {.inline.} =
if lhs.o != rhs.o:
false
elif lhs.o: # and rhs.o implied
lhs.v == rhs.v
else:
lhs.e == rhs.e
func `==`*[E0, E1](lhs: Result[void, E0], rhs: Result[void, E1]): bool {.inline.} =
if lhs.o != rhs.o:
false
elif lhs.o:
true
else:
lhs.e == rhs.e
func get*[T: not void, E](self: Result[T, E]): T {.inline.} =
## Fetch value of result if set, or raise Defect
## Exception bridge mode: raise given Exception instead
## See also: Option.get
assertOk(self)
self.v
func tryGet*[T: not void, E](self: Result[T, E]): T {.inline.} =
## Fetch value of result if set, or raise
## When E is an Exception, raise that exception - otherwise, raise a ResultError[E]
mixin raiseResultError
if not self.o: self.raiseResultError()
self.v
func get*[T, E](self: Result[T, E], otherwise: T): T {.inline.} =
## Fetch value of result if set, or return the value `otherwise`
## See `valueOr` for a template version that avoids evaluating `otherwise`
## unless necessary
if self.o: self.v
else: otherwise
func get*[T, E](self: var Result[T, E]): var T {.inline.} =
## Fetch value of result if set, or raise Defect
## Exception bridge mode: raise given Exception instead
## See also: Option.get
assertOk(self)
self.v
template `[]`*[T: not void, E](self: Result[T, E]): T =
## Fetch value of result if set, or raise Defect
## Exception bridge mode: raise given Exception instead
mixin get
self.get()
template `[]`*[T, E](self: var Result[T, E]): var T =
## Fetch value of result if set, or raise Defect
## Exception bridge mode: raise given Exception instead
mixin get
self.get()
template unsafeGet*[T, E](self: Result[T, E]): T =
## Fetch value of result if set, undefined behavior if unset
## See also: Option.unsafeGet
assert self.o
self.v
func expect*[T: not void, E](self: Result[T, E], m: string): T =
## Return value of Result, or raise a `Defect` with the given message - use
## this helper to extract the value when an error is not expected, for example
## because the program logic dictates that the operation should never fail
##
## ```nim
## let r = Result[int, int].ok(42)
## # Put here a helpful comment why you think this won't fail
## echo r.expect("r was just set to ok(42)")
## ```
if not self.o:
when E isnot void:
raiseResultDefect(m, self.e)
else:
raiseResultDefect(m)
self.v
func expect*[T: not void, E](self: var Result[T, E], m: string): var T =
if not self.o:
when E isnot void:
raiseResultDefect(m, self.e)
else:
raiseResultDefect(m)
self.v
func `$`*(self: Result): string =
## Returns string representation of `self`
if self.o: "Ok(" & $self.v & ")"
else: "Err(" & $self.e & ")"
func error*[T, E](self: Result[T, E]): E =
## Fetch error of result if set, or raise Defect
if self.o:
when T isnot void:
raiseResultDefect("Trying to access error when value is set", self.v)
else:
raiseResultDefect("Trying to access error when value is set")
self.e
template value*[T, E](self: Result[T, E]): T =
mixin get
self.get()
template value*[T, E](self: var Result[T, E]): T =
mixin get
self.get()
template valueOr*[T, E](self: Result[T, E], def: T): T =
## Fetch value of result if set, or supplied default
## default will not be evaluated iff value is set
if self.o: self.v
else: def
# void support
template ok*[E](R: type Result[void, E]): auto =
## Initialize a result with a success and value
## Example: `Result[int, string].ok(42)`
R(o: true)
template ok*[E](self: var Result[void, E]) =
## Set the result to success and update value
## Example: `result.ok(42)`
mixin ok
self = (type self).ok()
template ok*(): auto =
mixin ok
ok(typeof(result))
template err*(): auto =
mixin err
err(typeof(result))
# TODO:
# Supporting `map` and `get` operations on a `void` result is quite
# an unusual API. We should provide some motivating examples.
func map*[E, A](
self: Result[void, E], f: proc(): A): Result[A, E] {.inline.} =
## Transform value using f, or return error
if self.o: result.ok(f())
else: result.err(self.e)
func flatMap*[E, A](
self: Result[void, E], f: proc(): Result[A, E]): Result[A, E] {.inline.} =
if self.o: f(self.v)
else: Result[A, E].err(self.e)
func mapErr*[E, A](
self: Result[void, E], f: proc(x: E): A): Result[void, A] {.inline.} =
## Transform error using f, or return value
if self.o: result.ok()
else: result.err(f(self.e))
func map*[T, E](
self: Result[T, E], f: proc(x: T)): Result[void, E] {.inline.} =
## Transform value using f, or return error
if self.o: f(self.v); result.ok()
else: result.err(self.e)
func get*[E](self: Result[void, E]) {.inline.} =
## Fetch value of result if set, or raise
## See also: Option.get
mixin assertOk
assertOk(self)
func tryGet*[E](self: Result[void, E]) {.inline.} =
## Fetch value of result if set, or raise a CatchableError
mixin raiseResultError
if not self.o:
self.raiseResultError()
template `[]`*[E](self: Result[void, E]) =
## Fetch value of result if set, or raise
mixin get
self.get()
template unsafeGet*[E](self: Result[void, E]) =
## Fetch value of result if set, undefined behavior if unset
## See also: Option.unsafeGet
assert self.o
func expect*[E](self: Result[void, E], msg: string) =
if not self.o:
when E isnot void:
raiseResultDefect(msg, self.e)
else:
raiseResultDefect(msg)
func `$`*[E](self: Result[void, E]): string =
## Returns string representation of `self`
if self.o: "Ok()"
else: "Err(" & $self.e & ")"
template value*[E](self: Result[void, E]) =
mixin get
self.get()
template value*[E](self: var Result[void, E]) =
mixin get
self.get()
template `?`*[T, E](self: Result[T, E]): auto =
## Early return - if self is an error, we will return from the current
## function, else we'll move on..
##
## ```
## let v = ? funcWithResult()
## echo v # prints value, not Result!
## ```
## Experimental
# TODO the v copy is here to prevent multiple evaluations of self - could
# probably avoid it with some fancy macro magic..
let v = (self)
if not v.o:
when typeof(result) is typeof(v):
return v
else:
return err(typeof(result), v.e)
when not(T is void):
v.v