result: cleanups (#22)

* fix ResultError type
* add/fix documentation
* clean up raises (in preparation for better Defect handling)
* fix toException mixin
* work around compiler bug with more explicit types
* fix capture exception type
* fix result type on `?`
This commit is contained in:
Jacek Sieka 2020-03-30 22:49:13 +02:00 committed by GitHub
parent 4477f45c40
commit d622c07a08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 93 additions and 58 deletions

View File

@ -7,10 +7,10 @@
# at your option. This file may not be copied, modified, or distributed except according to those terms. # at your option. This file may not be copied, modified, or distributed except according to those terms.
type type
ResultError*[E] = ref object of ValueError ResultError*[E] = object of ValueError
## Error raised when trying to access value of result when error is set ## Error raised when trying to access value of result when error is set
## Note: If error is of exception type, it will be raised instead! ## Note: If error is of exception type, it will be raised instead!
error: E error*: E
Result*[T, E] = object Result*[T, E] = object
## Result type that can hold either a value or an error, but not both ## Result type that can hold either a value or an error, but not both
@ -19,7 +19,7 @@ type
## ##
## ``` ## ```
## # It's convenient to create an alias - most likely, you'll do just fine ## # It's convenient to create an alias - most likely, you'll do just fine
## # with strings as error! ## # with strings or cstrings as error
## ##
## type R = Result[int, string] ## type R = Result[int, string]
## ##
@ -49,10 +49,10 @@ type
## ##
## # If you provide this exception converter, this exception will be raised ## # If you provide this exception converter, this exception will be raised
## # on dereference ## # on dereference
## func toException(v: Error): ref CatchableException = (ref CatchableException)(msg: $v) ## func toException(v: Error): ref CatchableError = (ref CatchableError)(msg: $v)
## try: ## try:
## RE[int].err(a)[] ## RE[int].err(a)[]
## except CatchableException: ## except CatchableError:
## echo "in here!" ## echo "in here!"
## ##
## ``` ## ```
@ -92,6 +92,8 @@ type
## The API visibility issue of exceptions can also be solved with ## The API visibility issue of exceptions can also be solved with
## `{.raises.}` annotations - as of now, the compiler doesn't remind ## `{.raises.}` annotations - as of now, the compiler doesn't remind
## you to do so, even though it knows what the right annotation should be. ## 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 ## Many system languages make a distinction between errors you want to
## handle and those that are simply bugs or unrealistic to deal with.. ## handle and those that are simply bugs or unrealistic to deal with..
@ -117,6 +119,21 @@ type
## annotation that function may throw: ## annotation that function may throw:
## https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html ## 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 ## # Other implemenations in nim
## ##
## There are other implementations in nim that you might prefer: ## There are other implementations in nim that you might prefer:
@ -143,6 +160,11 @@ type
## * Rust uses From traits to deal with result translation as the result ## * Rust uses From traits to deal with result translation as the result
## travels up the call stack - needs more tinkering - some implicit ## travels up the call stack - needs more tinkering - some implicit
## conversions would be nice here ## conversions would be nice here
## * Pattern matching in rust allows convenient extraction of value or error
## in one go.
##
## Relevant nim bugs:
## https://github.com/nim-lang/Nim/issues/13799
case o: bool case o: bool
of false: of false:
@ -151,40 +173,42 @@ type
v: T v: T
func raiseResultError[T, E](self: Result[T, E]) {.noreturn.} = func raiseResultError[T, E](self: Result[T, E]) {.noreturn.} =
mixin toException
when E is ref Exception: when E is ref Exception:
if self.e.isNil: # for example Result.default()! if self.e.isNil: # for example Result.default()!
raise ResultError[void](msg: "Trying to access value with err (nil)") raise (ref ResultError[void])(msg: "Trying to access value with err (nil)")
raise self.e raise self.e
elif compiles(self.e.toException()): elif compiles(toException(self.e)):
raise self.e.toException() raise toException(self.e)
elif compiles($self.e): elif compiles($self.e):
raise ResultError[E]( raise (ref ResultError[E])(
error: self.e, msg: "Trying to access value with err: " & $self.e) error: self.e, msg: "Trying to access value with err: " & $self.e)
else: else:
raise ResultError[E](error: self.e) raise (res ResultError[E])(msg: "Trying to access value with err", error: self.e)
template ok*(R: type Result, x: auto): auto = template ok*[T, E](R: type Result[T, E], x: auto): R =
## Initialize a result with a success and value ## Initialize a result with a success and value
## Example: `Result[int, string].ok(42)` ## Example: `Result[int, string].ok(42)`
R(o: true, v: x) R(o: true, v: x)
template ok*(self: var Result, x: auto) = template ok*[T, E](self: var Result[T, E], x: auto) =
## Set the result to success and update value ## Set the result to success and update value
## Example: `result.ok(42)` ## Example: `result.ok(42)`
self = ok(type self, x) self = ok(type self, x)
template err*(R: type Result, x: auto): auto = template err*[T, E](R: type Result[T, E], x: auto): R =
## Initialize the result to an error ## Initialize the result to an error
## Example: `Result[int, string].err("uh-oh")` ## Example: `Result[int, string].err("uh-oh")`
R(o: false, e: x) R(o: false, e: x)
template err*(self: var Result, x: auto) = template err*[T, E](self: var Result[T, E], x: auto) =
## Set the result as an error ## Set the result as an error
## Example: `result.err("uh-oh")` ## Example: `result.err("uh-oh")`
self = err(type self, x) self = err(type self, x)
template ok*(v: auto): auto = typeof(result).ok(v) template ok*(v: auto): auto = ok(typeof(result), v)
template err*(v: auto): auto = typeof(result).err(v) template err*(v: auto): auto = err(typeof(result), v)
template isOk*(self: Result): bool = self.o template isOk*(self: Result): bool = self.o
template isErr*(self: Result): bool = not self.o template isErr*(self: Result): bool = not self.o
@ -220,32 +244,49 @@ func mapCast*[T0, E0](
if self.isOk: result.ok(cast[T1](self.v)) if self.isOk: result.ok(cast[T1](self.v))
else: result.err(self.e) else: result.err(self.e)
template `and`*(self: Result, other: untyped): untyped = template `and`*[T, E](self, other: Result[T, E]): Result[T, E] =
## Evaluate `other` iff self.isOk, else return error ## Evaluate `other` iff self.isOk, else return error
## fail-fast - will not evaluate other if a is an error ## fail-fast - will not evaluate other if a is an error
##
## TODO: This API is unsafe due to potential multiple
## evaluation of the `self` parameter.
if self.isOk: if self.isOk:
other other
else: else:
type R = type(other) type R = type(other)
R.err(self.e) R.err(self.e)
template `or`*(self: Result, other: untyped): untyped = template `or`*[T, E](self, other: Result[T, E]): Result[T, E] =
## Evaluate `other` iff not self.isOk, else return self ## Evaluate `other` iff not self.isOk, else return self
## fail-fast - will not evaluate other if a is a value ## fail-fast - will not evaluate other if a is a value
##
## TODO: This API is unsafe due to potential multiple
## evaluation of the `self` parameter.
if self.isOk: self if self.isOk: self
else: other else: other
template catch*(body: typed): Result[type(body), ref Exception] = template catch*(body: typed): Result[type(body), ref CatchableError] =
## Convert a try expression into a Result ## Catch exceptions for body and store them in the Result
type R = Result[type(body), ref Exception] ##
## ```
## let r = catch: someFuncThatMayRaise()
## ```
type R = Result[type(body), ref CatchableError]
try: try:
R.ok(body) R.ok(body)
except: except CatchableError as e:
R.err(getCurrentException()) R.err(e)
template capture*(T: type, e: ref Exception): Result[T, ref Exception] = template capture*[E: Exception](T: type, someExceptionExpr: ref E): Result[T, ref E] =
type R = Result[T, ref Exception] ## 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 var ret: R
try: try:
@ -253,12 +294,12 @@ template capture*(T: type, e: ref Exception): Result[T, ref Exception] =
# haven't actually tested... # haven't actually tested...
if true: if true:
# I'm sure there's a nicer way - this just works :) # I'm sure there's a nicer way - this just works :)
raise e raise someExceptionExpr
except: except E as caught:
ret = R.err(getCurrentException()) ret = R.err(caught)
ret ret
func `==`*(lhs, rhs: Result): bool {.inline.} = func `==`*[T0, E0, T1, E1](lhs: Result[T0, E0], rhs: Result[T1, E1]): bool {.inline.} =
if lhs.isOk != rhs.isOk: if lhs.isOk != rhs.isOk:
false false
elif lhs.isOk: elif lhs.isOk:
@ -307,7 +348,7 @@ func `$`*(self: Result): string =
else: "Err(" & $self.e & ")" else: "Err(" & $self.e & ")"
func error*[T, E](self: Result[T, E]): E = func error*[T, E](self: Result[T, E]): E =
if self.isOk: raise ResultError[void](msg: "Result does not contain an error") if self.isOk: raise (ref ResultError[void])(msg: "Result does not contain an error")
self.e self.e
@ -331,6 +372,13 @@ template ok*[E](self: var Result[void, E]) =
## Example: `result.ok(42)` ## Example: `result.ok(42)`
self = (type self).ok() self = (type self).ok()
template ok*(): auto = ok(typeof(result))
template err*(): auto = 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]( func map*[E, A](
self: Result[void, E], f: proc(): A): Result[A, E] {.inline.} = self: Result[void, E], f: proc(): A): Result[A, E] {.inline.} =
## Transform value using f, or return error ## Transform value using f, or return error
@ -379,11 +427,16 @@ template value*[E](self: var Result[void, E]) = self.get()
template `?`*[T, E](self: Result[T, E]): T = template `?`*[T, E](self: Result[T, E]): T =
## Early return - if self is an error, we will return from the current ## Early return - if self is an error, we will return from the current
## function, else we'll move on.. ## function, else we'll move on..
##
## ```
## let v = ? funcWithResult()
## echo v # prints value, not Result!
## ```
## Experimental ## Experimental
# TODO the v copy is here to prevent multiple evaluations of self - could # TODO the v copy is here to prevent multiple evaluations of self - could
# probably avoid it with some fancy macro magic.. # probably avoid it with some fancy macro magic..
let v = self let v = (self)
if v.isErr: return v if v.isErr: return err(typeof(result), v.error)
v.value v.value

View File

@ -10,7 +10,7 @@ func fails(): R = R.err("dummy")
func fails2(): R = result.err("dummy") func fails2(): R = result.err("dummy")
func raises(): int = func raises(): int =
raise newException(Exception, "hello") raise (ref CatchableError)(msg: "hello")
# Basic usage, consumer # Basic usage, consumer
let let
@ -83,13 +83,15 @@ doAssert (rOk.flatMap(
doAssert (rErr.mapErr(func(x: string): string = x & "no!").error == (rErr.error & "no!")) doAssert (rErr.mapErr(func(x: string): string = x & "no!").error == (rErr.error & "no!"))
# Exception interop # Exception interop
let e = capture(int, newException(Exception, "test")) let e = capture(int, (ref ValueError)(msg: "test"))
doAssert e.isErr doAssert e.isErr
doAssert e.error.msg == "test"
try: try:
discard e[] discard e[]
doAssert false, "should have raised" doAssert false, "should have raised"
except: except ValueError as e:
doAssert getCurrentException().msg == "test" doAssert e.msg == "test"
# Nice way to checks # Nice way to checks
if (let v = works(); v.isOk): if (let v = works(); v.isOk):
@ -145,33 +147,13 @@ func testErr(): Result[int, string] =
doAssert testOk()[] == 42 doAssert testOk()[] == 42
doAssert testErr().error == "323" doAssert testErr().error == "323"
# It's also possible to use the same trick for stack capture:
template capture*(): untyped =
type R = type(result)
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 newException(Exception, "")
except:
ret = R.err(getCurrentException())
ret
proc testCapture(): Result[int, ref Exception] =
return capture()
doAssert testCapture().isErr
func testQn(): Result[int, string] = func testQn(): Result[int, string] =
let x = ?works() - ?works() let x = ?works() - ?works()
result.ok(x) result.ok(x)
func testQn2(): Result[int, string] = func testQn2(): Result[int, string] =
# looks like we can even use it creatively like this # looks like we can even use it creatively like this
if ?fails() == 42: raise newException(Exception, "shouldn't happen") if ?fails() == 42: raise (ref ValueError)(msg: "shouldn't happen")
doAssert testQn()[] == 0 doAssert testQn()[] == 0
doAssert testQn2().isErr doAssert testQn2().isErr
@ -180,7 +162,7 @@ type
AnEnum = enum AnEnum = enum
anEnumA anEnumA
anEnumB anEnumB
AnException = ref object of Exception AnException = ref object of CatchableError
v: AnEnum v: AnEnum
func toException(v: AnEnum): AnException = AnException(v: v) func toException(v: AnEnum): AnException = AnException(v: v)