diff --git a/stew/results.nim b/stew/results.nim index 03c29dd..9f3832f 100644 --- a/stew/results.nim +++ b/stew/results.nim @@ -22,8 +22,13 @@ type ## # Example ## ## ``` + ## import stew/results + ## + ## # Re-export `results` so that API is always available to users of your module! + ## export results + ## ## # It's convenient to create an alias - most likely, you'll do just fine - ## # with strings or cstrings as error + ## # with strings or cstrings as error for a start ## ## type R = Result[int, string] ## @@ -33,9 +38,13 @@ type ## # ok says it went... ok! ## R.ok 42 ## func fails(): R = - ## # or type it like this, to not repeat the type! + ## # or type it like this, to not repeat the type: ## result.err "bad luck" ## + ## func alsoWorks(): R = + ## # or just use the shortcut - auto-deduced from the return type! + ## ok(24) + ## ## if (let w = works(); w.isOk): ## echo w[], " or use value: ", w.value ## @@ -45,20 +54,29 @@ type ## 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: + ## # You can use the question mark operator to pass errors up the call 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 + ## # If you provide this exception converter, this exception will be raised on + ## # `tryGet`: ## func toException(v: Error): ref CatchableError = (ref CatchableError)(msg: $v) ## try: - ## RE[int].err(a)[] + ## RE[int].err(a).tryGet() ## except CatchableError: ## echo "in here!" ## + ## # You can use `Opt[T]` as a replacement for `Option` = `Opt` is an alias for + ## # `Result[T, void]`, meaning you can use the full `Result` API on it: + ## let x = Opt[int].ok(42) + ## echo x.get() + ## + ## # ... or `Result[void, E]` as a replacement for `bool`, providing extra error + ## # information! + ## let y = Resul[void, string].err("computation failed") + ## echo y.error() + ## ## ``` ## ## See the tests for more practical examples, specially when working with @@ -144,16 +162,53 @@ type ## 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 + ## A natural "error API" progression is starting with `Opt[T]`, then ## `Result[T, cstring]`, `Result[T, enum]` and `Result[T, object]` in ## escalating order of complexity. ## + ## # Result equivalences with other types + ## + ## Result allows tightly controlling the amount of information that a + ## function gives to the caller: + ## + ## ## `Result[void, void] == bool` + ## + ## Neither value nor error information, it either worked or didn't. Most + ## often used for `proc`:s with side effects. + ## + ## ## `Result[T, void] == Option[T]` + ## + ## Return value if it worked, else tell the caller it failed. Most often + ## used for simple computiations. + ## + ## Works as a fully replacement for `Option[T]` (aliased as `Opt[T]`) + ## + ## ## `Result[T, E]` - + ## + ## Return value if it worked, or a statically known piece of information + ## when it didn't - most often used when a function can fail in more than + ## one way - E is typically a `string` or an `enum`. + ## + ## ## `Result[T, ref E]` + ## + ## Returning a `ref E` allows introducing dynamically typed error + ## information, similar to exceptions. + ## ## # 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/ ## + ## `Option` compatibility + ## + ## `Result[T, void]` is similar to `Option[T]`, except it can be used with + ## all `Result` operators and helpers. + ## + ## One difference is `Option[ref|ptr T]` which disallows `nil` - `Opt[T]` + ## allows an "ok" result to hold `nil` - this can be useful when `nil` is + ## a valid outcome of a function, but increases complexity for the caller. + ## ## # Implementation notes ## ## This implementation is mostly based on the one in rust. Compared to it, @@ -257,6 +312,14 @@ type Opt*[T] = Result[T, void] +func raiseResultOk[T, E](self: Result[T, E]) {.noreturn, noinline.} = + # noinline because raising should take as little space as possible at call + # site + when T is void: + raise (ref ResultError[void])(msg: "Trying to access error with value") + else: + raise (ref ResultError[T])(msg: "Trying to access error with value", error: self.v) + func raiseResultError[T, E](self: Result[T, E]) {.noreturn, noinline.} = # noinline because raising should take as little space as possible at call # site @@ -266,13 +329,15 @@ func raiseResultError[T, E](self: Result[T, E]) {.noreturn, noinline.} = if self.e.isNil: # for example Result.default()! raise (ref ResultError[void])(msg: "Trying to access value with err (nil)") raise self.e + elif E is void: + raise (ref ResultError[void])(msg: "Trying to access value with err") 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) + error: self.e, msg: $self.e) else: - raise (res ResultError[E])(msg: "Trying to access value with err", error: self.e) + raise (ref ResultError[E])(msg: "Trying to access value with err", error: self.e) func raiseResultDefect(m: string, v: auto) {.noreturn, noinline.} = mixin `$` @@ -283,23 +348,34 @@ func raiseResultDefect(m: string) {.noreturn, noinline.} = raise (ref ResultDefect)(msg: m) template assertOk(self: Result) = + # Careful - `self` evaluated multiple times, which is fine in all current uses 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 = +template ok*[T, E](R: type Result[T, E], x: untyped): 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) = +template ok*[E](R: type Result[void, E]): R = + ## Initialize a result with a success and value + ## Example: `Result[void, string].ok()` + R(o: true) + +template ok*[T: not void, E](self: var Result[T, E], x: untyped) = ## 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 = +template ok*[E](self: var Result[void, E]) = + ## Set the result to success and update value + ## Example: `result.ok()` + self = (type self).ok() + +template err*[T, E](R: type Result[T, E], x: untyped): R = ## Initialize the result to an error ## Example: `Result[int, string].err("uh-oh")` R(o: false, e: x) @@ -307,13 +383,15 @@ template err*[T, E](R: type Result[T, E], x: auto): R = 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 + const s = x # avoid dangling cstring pointers R(o: false, e: cstring(s)) template err*[T](R: type Result[T, void]): R = + ## Initialize the result to an error + ## Example: `Result[int, void].err()` R(o: false) -template err*[T, E](self: var Result[T, E], x: auto) = +template err*[T, E](self: var Result[T, E], x: untyped) = ## Set the result as an error ## Example: `result.err("uh-oh")` self = err(type self, x) @@ -328,59 +406,164 @@ template err*[T](self: var Result[T, void]) = self = err(type self) template ok*(v: auto): auto = ok(typeof(result), v) +template ok*(): auto = ok(typeof(result)) + template err*(v: auto): auto = err(typeof(result), v) +template err*(): auto = err(typeof(result)) 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.} = +func map*[T0, E, T1]( + self: Result[T0, E], f: proc(x: T0): T1): Result[T1, 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) + if self.o: + result.ok(f(self.v)) + else: + when E is void: + result.err() + 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.} = +func map*[T, E]( + self: Result[T, E], f: proc(x: T)): Result[void, 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: + f(self.v) + result.ok() + else: + when E is void: + result.err() + else: + result.err(self.e) + +func map*[E, T1]( + self: Result[void, E], f: proc(): T1): Result[T1, E] {.inline.} = + ## Transform value using f, or return error + if self.o: + result.ok(f()) + else: + when E is void: + result.err() + else: + result.err(self.e) + +func map*[E]( + self: Result[void, E], f: proc()): Result[void, E] {.inline.} = + ## Call f if value is + if self.o: + f() + result.ok() + else: + when E is void: + result.err() + else: + result.err(self.e) + +func flatMap*[T0, E, T1]( + self: Result[T0, E], f: proc(x: T0): Result[T1, E]): Result[T1, E] {.inline.} = if self.o: f(self.v) - else: Result[A, E].err(self.e) + else: + when E is void: + Result[T1, void].err() + else: + Result[T1, E].err(self.e) -func mapErr*[T: not void, E, A]( - self: Result[T, E], f: proc(x: E): A): Result[T, A] {.inline.} = +func flatMap*[E, T1]( + self: Result[void, E], f: proc(): Result[T1, E]): Result[T1, E] {.inline.} = + if self.o: f() + else: + when E is void: + Result[T1, void].err() + else: + Result[T1, E].err(self.e) + +func mapErr*[T, E0, E1]( + self: Result[T, E0], f: proc(x: E0): E1): Result[T, E1] {.inline.} = + ## Transform error using f, or leave untouched + if self.o: + when T is void: + result.ok() + else: + result.ok(self.v) + else: + result.err(f(self.e)) + +func mapErr*[T, E1]( + self: Result[T, void], f: proc(): E1): Result[T, E1] {.inline.} = ## Transform error using f, or return value - if self.o: result.ok(self.v) - else: result.err(f(self.e)) + if self.o: + when T is void: + result.ok() + else: + result.ok(self.v) + else: + result.err(f()) -func mapConvert*[T0, E0]( - self: Result[T0, E0], T1: type): Result[T1, E0] {.inline.} = +func mapErr*[T, E0]( + self: Result[T, E0], f: proc(x: E0)): Result[T, void] {.inline.} = + ## Transform error using f, or return value + if self.o: + when T is void: + result.ok() + else: + result.ok(self.v) + else: + f(self.e) + result.err() + +func mapErr*[T]( + self: Result[T, void], f: proc()): Result[T, void] {.inline.} = + ## Transform error using f, or return value + if self.o: + when T is void: + result.ok() + else: + result.ok(self.v) + else: + f() + result.err() + +func mapConvert*[T0, E]( + self: Result[T0, E], T1: type): Result[T1, E] {.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) + if self.o: + when T1 is void: + result.ok() + else: + result.ok(T1(self.v)) + else: + when E is void: + result.err() + else: + result.err(self.e) -func mapCast*[T0, E0]( - self: Result[T0, E0], T1: type): Result[T1, E0] {.inline.} = +func mapCast*[T0, E]( + self: Result[T0, E], T1: type): Result[T1, E] {.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) + else: + when E is void: + result.err() + 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 + let s = (self) # TODO avoid copy if s.o: other else: @@ -388,7 +571,10 @@ template `and`*[T0, E, T1](self: Result[T0, E], other: Result[T1, E]): Result[T1 s else: type R = type(other) - err(R, s.e) + when E is void: + err(R) + else: + 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` @@ -396,18 +582,45 @@ template `or`*[T, E0, E1](self: Result[T, E0], other: Result[T, E1]): Result[T, ## ## ``` ## func f(): Result[int, SomeEnum] = - ## f2() or err(EnumValue) # Collapse errors from other module / function + ## f2() or err(SomeEnum.V) # Collapse errors from other module / function ## ``` - let s = self + let s = (self) # TODO avoid copy if s.o: when type(self) is type(other): s else: type R = type(other) - ok(R, s.v) + when T is void: + ok(R) + else: + ok(R, s.v) else: other +template orErr*[T, E0, E1](self: Result[T, E0], error: E1): Result[T, E1] = + ## Evaluate `other` iff `not self.isOk`, else return `self` + ## fail-fast - will not evaluate `error` if `self` is ok + ## + ## ``` + ## func f(): Result[int, SomeEnum] = + ## f2().orErr(SomeEnum.V) # Collapse errors from other module / function + ## ``` + ## + ## ** Experimental, may be removed ** + let s = (self) # TODO avoid copy + type R = Result[T, E1] + if s.o: + when type(self) is R: + s + else: + when T is void: + ok(R) + else: + ok(R, s.v) + else: + err(R, error) + + template catch*(body: typed): Result[type(body), ref CatchableError] = ## Catch exceptions for body and store them in the Result ## @@ -442,7 +655,10 @@ template capture*[E: Exception](T: type, someExceptionExpr: ref E): Result[T, re ret = R.err(caught) ret -func `==`*[T0: not void, E0, T1: not void, E1](lhs: Result[T0, E0], rhs: Result[T1, E1]): bool {.inline.} = +func `==`*[ + T0: not void, E0: not void, + T1: not void, E1: not void]( + lhs: Result[T0, E0], rhs: Result[T1, E1]): bool {.inline.} = if lhs.o != rhs.o: false elif lhs.o: # and rhs.o implied @@ -450,27 +666,39 @@ func `==`*[T0: not void, E0, T1: not void, E1](lhs: Result[T0, E0], rhs: Result[ else: lhs.e == rhs.e -func `==`*[E0, E1](lhs: Result[void, E0], rhs: Result[void, E1]): bool {.inline.} = +func `==`*[E0, E1]( + lhs: Result[void, E0], rhs: Result[void, E1]): bool {.inline.} = if lhs.o != rhs.o: false - elif lhs.o: + elif lhs.o: # and rhs.o implied true else: lhs.e == rhs.e -func get*[T: not void, E](self: Result[T, E]): T {.inline.} = +func `==`*[T0, T1]( + lhs: Result[T0, void], rhs: Result[T1, void]): bool {.inline.} = + if lhs.o != rhs.o: + false + elif lhs.o: # and rhs.o implied + lhs.v == rhs.v + else: + true + +func get*[T, 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 + when T isnot void: + self.v -func tryGet*[T: not void, E](self: Result[T, E]): T {.inline.} = +func tryGet*[T, 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 + when T isnot void: + 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` @@ -479,33 +707,34 @@ func get*[T, E](self: Result[T, E], otherwise: T): T {.inline.} = if self.o: self.v else: otherwise -func get*[T, E](self: var Result[T, E]): var T {.inline.} = +func get*[T: not void, 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 = +template `[]`*[T, 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 = +template `[]`*[T: not void, 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 = +template unsafeGet*[T: not void, E](self: Result[T, E]): T = ## Fetch value of result if set, undefined behavior if unset - ## See also: Option.unsafeGet - assert self.o - + ## See also: `unsafeError` self.v -func expect*[T: not void, E](self: Result[T, E], m: string): T = +template unsafeGet*[E](self: Result[void, E]) = + ## Fetch value of result if set, undefined behavior if unset + ## See also: `unsafeError` + assert self.o + +func expect*[T, 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 @@ -520,7 +749,8 @@ func expect*[T: not void, E](self: Result[T, E], m: string): T = raiseResultDefect(m, self.e) else: raiseResultDefect(m) - self.v + when T isnot void: + self.v func expect*[T: not void, E](self: var Result[T, E], m: string): var T = if not self.o: @@ -530,10 +760,14 @@ func expect*[T: not void, E](self: var Result[T, E], m: string): var T = raiseResultDefect(m) self.v -func `$`*(self: Result): string = +func `$`*[T, E](self: Result[T, E]): string = ## Returns string representation of `self` - if self.o: "Ok(" & $self.v & ")" - else: "Err(" & $self.e & ")" + if self.o: + when T is void: "ok()" + else: "ok(" & $self.v & ")" + else: + when E is void: "err()" + else: "err(" & $self.e & ")" func error*[T, E](self: Result[T, E]): E = ## Fetch error of result if set, or raise Defect @@ -542,111 +776,112 @@ func error*[T, E](self: Result[T, E]): E = raiseResultDefect("Trying to access error when value is set", self.v) else: raiseResultDefect("Trying to access error when value is set") + when E isnot void: + self.e + +func tryError*[T, E](self: Result[T, E]): E {.inline.} = + ## Fetch error of result if set, or raise + ## Raises a ResultError[T] + mixin raiseResultOk + if self.o: self.raiseResultOk() + when E isnot void: + self.e + +template unsafeError*[T, E: not void](self: Result[T, E]): E = + ## Fetch value of result if set, undefined behavior if unset + ## See also: `unsafeGet` self.e -template value*[T, E](self: Result[T, E]): T = - mixin get - self.get() +template unsafeError*[T](self: Result[T, void]) = + ## Fetch value of result if set, undefined behavior if unset + ## See also: `unsafeGet` + assert not self.o # Emulate field access defect in debug builds -template value*[T, E](self: var Result[T, E]): T = - mixin get - self.get() +# Alternative spellings for get +template value*[T, E](self: Result[T, E]): T = self.get() +template value*[T: not void, E](self: var Result[T, E]): var T = 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 +template valueOr*[T: not void, E](self: Result[T, E], def: untyped): T = + ## Fetch value of result if set, or evaluate `def` + ## `def` is evaluated lazily, and must be an expression of `T` or exit + ## the scope (for example using `return` / `raise`) + ## + ## Example: + ## ``` + ## let + ## v = Result[int, string].err("hello") + ## x = v.valueOr: 42 # x == 42 now + ## y = v.valueOr: raise (ref ValueError)(msg: "v is an error, gasp!") + ## ``` + let s = (self) # TODO avoid copy + if s.o: s.v else: def -# void support +template errorOr*[T: not void, E](self: Result[T, E], def: untyped): E = + ## Fetch error of result if not set, or evaluate `def` + ## `def` is evaluated lazily, and must be an expression of `T` or exit + ## the scope (for example using `return` / `raise`) + let s = (self) # TODO avoid copy + if not s.o: s.e + else: def -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) +func flatten*[T, E](self: Result[Result[T, E], E]): Result[T, E] = + ## Remove one level of nesting + if self.o: + self.v + else: + when E is void: + err(Result[T, E]) else: - raiseResultDefect(msg) + err(Result[T, E], self.error) -func `$`*[E](self: Result[void, E]): string = - ## Returns string representation of `self` - if self.o: "Ok()" - else: "Err(" & $self.e & ")" +func filter*[T, E]( + self: Result[T, E], + callback: proc(x: T): Result[void, E]): Result[T, E] = + ## Apply `callback` to the `self`, iff `self` is not an error. If `callback` + ## returns an error, return that error, else return `self` -template value*[E](self: Result[void, E]) = - mixin get - self.get() + if self.o: + callback(self.v) and self + else: + self -template value*[E](self: var Result[void, E]) = - mixin get - self.get() +func filter*[E]( + self: Result[void, E], + callback: proc(): Result[void, E]): Result[void, E] = + ## Apply `callback` to the `self`, iff `self` is not an error. If `callback` + ## returns an error, return that error, else return `self` + + if self.o: + callback() and self + else: + self + +func filter*[T]( + self: Result[T, void], + callback: proc(x: T): bool): Result[T, void] = + ## Apply `callback` to the `self`, iff `self` is not an error. If `callback` + ## returns an error, return that error, else return `self` + + if self.o: + if callback(self.v): + self + else: + Result[T, void].err() + else: + self + +# Options compatibility + +template isSome*(o: Opt): bool = + ## Alias for `isOk` + isOk o + +template isNone*(o: Opt): bool = + ## Alias of `isErr` + isErr o + +# Syntactic convenience template `?`*[T, E](self: Result[T, E]): auto = ## Early return - if self is an error, we will return from the current @@ -664,7 +899,10 @@ template `?`*[T, E](self: Result[T, E]): auto = when typeof(result) is typeof(v): return v else: - return err(typeof(result), v.e) + when E is void: + return err(typeof(result)) + else: + return err(typeof(result), v.e) when not(T is void): v.v diff --git a/tests/test_results.nim b/tests/test_results.nim index 2465a2c..8e5bd28 100644 --- a/tests/test_results.nim +++ b/tests/test_results.nim @@ -1,197 +1,180 @@ # nim-result is also available stand-alone from https://github.com/arnetheduck/nim-result/ import ../stew/results + type R = Result[int, string] # Basic usage, producer -func works(): R = R.ok(42) -func works2(): R = result.ok(42) -func fails(): R = R.err("dummy") -func fails2(): R = result.err("dummy") -func raises(): int = - raise (ref CatchableError)(msg: "hello") +block: + func works(): R = R.ok(42) + func works2(): R = result.ok(42) + func works3(): R = ok(42) -# Basic usage, consumer -let - rOk = works() - rOk2 = works2() - rErr = fails() - rErr2 = fails2() + func fails(): R = R.err("dummy") + func fails2(): R = result.err("dummy") + func fails3(): R = err("dummy") -doAssert rOk.isOk -doAssert rOk2.isOk -doAssert rOk.get() == 42 -doAssert (not rOk.isErr) -doAssert rErr.isErr -doAssert rErr2.isErr + let + rOk = works() + rOk2 = works2() + rOk3 = works3() -# Combine -doAssert (rOk and rErr).isErr -doAssert (rErr and rOk).isErr -doAssert (rOk or rErr).isOk -doAssert (rErr or rOk).isOk + rErr = fails() + rErr2 = fails2() + rErr3 = fails3() -# `and` heterogenous types -doAssert (rOk and rOk.map(proc(x: auto): auto = $x))[] == $(rOk[]) + doAssert rOk.isOk + doAssert rOk2.isOk + doAssert rOk3.isOk + doAssert (not rOk.isErr) -# `or` heterogenous types -doAssert (rErr or rErr.mapErr(proc(x: auto): auto = len(x))).error == len(rErr.error) + doAssert rErr.isErr + doAssert rErr2.isErr + doAssert rErr3.isErr -# Exception on access -let va = try: discard rOk.error; false except: true -doAssert va, "not an error, should raise" + # Mutate + var x = rOk + x.err("failed now") + doAssert x.isErr + doAssert x.error == "failed now" -# Exception on access -let vb = try: discard rErr.value; false except: true -doAssert vb, "not an value, should raise" + # Combine + doAssert (rOk and rErr).isErr + doAssert (rErr and rOk).isErr + doAssert (rOk or rErr).isOk + doAssert (rErr or rOk).isOk -var x = rOk + # Fail fast + proc failFast(): int = raiseAssert "shouldn't evaluate" + proc failFastR(): R = raiseAssert "shouldn't evaluate" -# Mutate -x.err("failed now") + doAssert (rErr and failFastR()).isErr + doAssert (rOk or failFastR()).isOk -doAssert x.isErr + # `and` heterogenous types + doAssert (rOk and Result[string, string].ok($rOk.get())).get() == $(rOk[]) -# Exceptions -> results -let c = catch: - raises() + # `or` heterogenous types + doAssert (rErr or Result[int, int].err(len(rErr.error))).error == len(rErr.error) -doAssert c.isErr + # Exception on access + doAssert (try: (discard rOk.tryError(); false) except ResultError[int]: true) + doAssert (try: (discard rErr.tryGet(); false) except ResultError[string]: true) -# De-reference -try: - echo rErr[] - doAssert false -except: - discard + # Value access or default + doAssert rOk.get(100) == rOk.get() + doAssert rErr.get(100) == 100 -doAssert rOk.valueOr(50) == rOk.value -doAssert rErr.valueOr(50) == 50 + doAssert rOk.get() == rOk.unsafeGet() -# Comparisons -doAssert (works() == works2()) -doAssert (fails() == fails2()) -doAssert (works() != fails()) + doAssert rOk.valueOr(failFast()) == rOk.value() + let rErrV = rErr.valueOr: 100 + doAssert rErrV == 100 -var counter = 0 -proc incCounter(): R = - counter += 1 - R.ok(counter) + let rOkV = rOk.errorOr: "quack" + doAssert rOkV == "quack" -doAssert (rErr and incCounter()).isErr, "b fails" -doAssert counter == 0, "should fail fast on rErr" + # Exceptions -> results + func raises(): int = + raise (ref CatchableError)(msg: "hello") -# Mapping -doAssert (rOk.map(func(x: int): string = $x)[] == $rOk.value) -doAssert (rOk.flatMap( - proc(x: int): Result[string, string] = Result[string, string].ok($x))[] == $rOk.value) -doAssert (rErr.mapErr(func(x: string): string = x & "no!").error == (rErr.error & "no!")) + let c = catch: + raises() + doAssert c.isErr -# Exception interop -let e = capture(int, (ref ValueError)(msg: "test")) -doAssert e.isErr -doAssert e.error.msg == "test" + # De-reference + try: + echo rErr[] + doAssert false + except: + discard -try: - discard e.tryGet - doAssert false, "should have raised" -except ValueError as e: - doAssert e.msg == "test" + # Comparisons + doAssert (rOk == rOk) + doAssert (rErr == rErr) + doAssert (rOk != rErr) -# Nice way to checks -if (let v = works(); v.isOk): - doAssert v[] == v.value + # Mapping + doAssert (rOk.map(func(x: int): string = $x)[] == $rOk.value) + doAssert (rOk.map(func(x: int) = discard)).isOk() -# Can formalise it into a template (https://github.com/arnetheduck/nim-result/issues/8) -template `?=`*(v: untyped{nkIdent}, vv: Result): bool = - (let vr = vv; template v: auto {.used.} = unsafeGet(vr); vr.isOk) -if f ?= works(): - doAssert f == works().value + doAssert (rOk.flatMap( + proc(x: int): Result[string, string] = Result[string, string].ok($x))[] == $rOk.value) -doAssert $rOk == "Ok(42)" + doAssert (rErr.mapErr(func(x: string): string = x & "no!").error == (rErr.error & "no!")) -doAssert rOk.mapConvert(int64)[] == int64(42) -doAssert rOk.mapCast(int8)[] == int8(42) -doAssert rOk.mapConvert(uint64)[] == uint64(42) + # Casts and conversions + doAssert rOk.mapConvert(int64)[] == int64(42) + doAssert rOk.mapConvert(uint64)[] == uint64(42) + doAssert rOk.mapCast(int8)[] == int8(42) -try: - discard rErr.get() - doAssert false -except Defect: # TODO catching defects is undefined behaviour, use external test suite? - discard + doAssert (rErr.orErr(32)).error == 32 + doAssert (rOk.orErr(failFast())).get() == rOk.get() -try: - discard rOk.error() - doAssert false -except Defect: # TODO catching defects is undefined behaviour, use external test suite? - discard + # string conversion + doAssert $rOk == "ok(42)" + doAssert $rErr == "err(dummy)" -# TODO there's a bunch of operators that one could lift through magic - this -# is mainly an example -template `+`*(self, other: Result): untyped = - ## Perform `+` on the values of self and other, if both are ok - type R = type(other) - if self.isOk: - if other.isOk: - R.ok(self.value + other.value) - else: - R.err(other.error) - else: - R.err(self.error) + # Exception interop + let e = capture(int, (ref ValueError)(msg: "test")) + doAssert e.isErr + doAssert e.error.msg == "test" -# Simple lifting.. -doAssert (rOk + rOk)[] == rOk.value + rOk.value + try: + discard rOk.tryError() + doAssert false, "should have raised" + except ValueError: + discard -iterator items[T, E](self: Result[T, E]): T = - ## Iterate over result as if it were a collection of either 0 or 1 items - ## TODO should a Result[seq[X]] iterate over items in seq? there are - ## arguments for and against - if self.isOk: - yield self.value + try: + discard e.tryGet() + doAssert false, "should have raised" + except ValueError as e: + doAssert e.msg == "test" -# Iteration -var counter2 = 0 -for v in rOk: - counter2 += 1 + # Nice way to checks + if (let v = works(); v.isOk): + doAssert v[] == v.value -doAssert counter2 == 1, "one-item collection when set" + # Expectations + doAssert rOk.expect("testOk never fails") == 42 -func testOk(): Result[int, string] = - ok 42 + # Question mark operator + func testQn(): Result[int, string] = + let x = ?works() - ?works() + ok(x) -func testErr(): Result[int, string] = - err "323" + func testQn2(): Result[int, string] = + # looks like we can even use it creatively like this + if ?fails() == 42: raise (ref ValueError)(msg: "shouldn't happen") -doAssert testOk()[] == 42 -doAssert testErr().error == "323" + func testQn3(): Result[bool, string] = + # different T but same E + let x = ?works() - ?works() + ok(x == 0) -doAssert testOk().expect("testOk never fails") == 42 + doAssert testQn()[] == 0 + doAssert testQn2().isErr + doAssert testQn3()[] -func testQn(): Result[int, string] = - let x = ?works() - ?works() - result.ok(x) + proc heterOr(): Result[int, int] = + let value = ? (rErr or err(42)) # TODO ? binds more tightly than `or` - can that be fixed? + doAssert value + 1 == value, "won't reach, ? will shortcut execution" + ok(value) -func testQn2(): Result[int, string] = - # looks like we can even use it creatively like this - if ?fails() == 42: raise (ref ValueError)(msg: "shouldn't happen") + doAssert heterOr().error() == 42 -func testQn3(): Result[bool, string] = - # different T but same E - let x = ?works() - ?works() - result.ok(x == 0) + # Flatten + doAssert Result[R, string].ok(rOk).flatten() == rOk + doAssert Result[R, string].ok(rErr).flatten() == rErr -doAssert testQn()[] == 0 -doAssert testQn2().isErr -doAssert testQn3()[] - -proc heterOr(): Result[int, int] = - let value = ? (rErr or err(42)) # TODO ? binds more tightly than `or` - can that be fixed? - doAssert value + 1 == value, "won't reach, ? will shortcut execution" - ok(value) - -doAssert heterOr().error() == 42 + # Filter + doAssert rOk.filter(proc(x: int): auto = Result[void, string].ok()) == rOk + doAssert rOk.filter(proc(x: int): auto = Result[void, string].err("filter")).error == "filter" + doAssert rErr.filter(proc(x: int): auto = Result[void, string].err("filter")) == rErr +# Exception conversions - toException must not be inside a block type AnEnum = enum anEnumA @@ -224,68 +207,192 @@ func testToString(): int = doAssert testToString() == 42 -type VoidRes = Result[void, int] +block: # Result[void, E] + type VoidRes = Result[void, int] -func worksVoid(): VoidRes = VoidRes.ok() -func worksVoid2(): VoidRes = result.ok() -func failsVoid(): VoidRes = VoidRes.err(42) -func failsVoid2(): VoidRes = result.err(42) + func worksVoid(): VoidRes = VoidRes.ok() + func worksVoid2(): VoidRes = result.ok() + func worksVoid3(): VoidRes = ok() -let - vOk = worksVoid() - vOk2 = worksVoid2() - vErr = failsVoid() - vErr2 = failsVoid2() + func failsVoid(): VoidRes = VoidRes.err(42) + func failsVoid2(): VoidRes = result.err(42) + func failsVoid3(): VoidRes = err(42) -doAssert vOk.isOk -doAssert vOk2.isOk -doAssert vErr.isErr -doAssert vErr2.isErr + let + vOk = worksVoid() + vOk2 = worksVoid2() + vOk3 = worksVoid3() -vOk.get() -vOk.expect("should never fail") + vErr = failsVoid() + vErr2 = failsVoid2() + vErr3 = failsVoid3() -doAssert vOk.map(proc (): int = 42).get() == 42 + doAssert vOk.isOk + doAssert vOk2.isOk + doAssert vOk3.isOk + doAssert (not vOk.isErr) -rOk.map(proc(x: int) = discard).get() + doAssert vErr.isErr + doAssert vErr2.isErr + doAssert vErr3.isErr -try: - rErr.map(proc(x: int) = discard).get() - doAssert false -except: - discard + vOk.get() + vOk.unsafeGet() + vOk.expect("should never fail") -doAssert vErr.mapErr(proc(x: int): int = 10).error() == 10 + # Comparisons + doAssert (vOk == vOk) + doAssert (vErr == vErr) + doAssert (vOk != vErr) -func voidF(): VoidRes = - ok() + # Mapping + doAssert vOk.map(proc (): int = 42).get() == 42 + vOk.map(proc () = discard).get() -func voidF2(): VoidRes = - ? voidF() + vOk.mapErr(proc(x: int): int = 10).get() + vOk.mapErr(proc(x: int) = discard).get() - ok() + doAssert vErr.mapErr(proc(x: int): int = 10).error() == 10 -doAssert voidF2().isOk + # string conversion + doAssert $vOk == "ok()" + doAssert $vErr == "err(42)" + # Question mark operator + func voidF(): VoidRes = + ok() -type CSRes = Result[void, cstring] + func voidF2(): Result[int, int] = + ? voidF() -func cstringF(s: string): CSRes = - when compiles(err(s)): - doAssert false + ok(42) -discard cstringF("test") + doAssert voidF2().isOk -# Compare void -block: - var a, b: Result[void, bool] - doAssert a == b + # flatten + doAssert Result[VoidRes, int].ok(vOk).flatten() == vOk + doAssert Result[VoidRes, int].ok(vErr).flatten() == vErr - a.ok() + # Filter + doAssert vOk.filter(proc(): auto = Result[void, int].ok()) == vOk + doAssert vOk.filter(proc(): auto = Result[void, int].err(100)).error == 100 + doAssert vErr.filter(proc(): auto = Result[void, int].err(100)) == vErr - doAssert not (a == b) - doAssert not (b == a) +block: # Result[T, void] aka `Opt` + type OptInt = Result[int, void] - b.ok() + func worksOpt(): OptInt = OptInt.ok(42) + func worksOpt2(): OptInt = result.ok(42) + func worksOpt3(): OptInt = ok(42) + + func failsOpt(): OptInt = OptInt.err() + func failsOpt2(): OptInt = result.err() + func failsOpt3(): OptInt = err() + + let + oOk = worksOpt() + oOk2 = worksOpt2() + oOk3 = worksOpt3() + + oErr = failsOpt() + oErr2 = failsOpt2() + oErr3 = failsOpt3() + + doAssert oOk.isOk + doAssert oOk2.isOk + doAssert oOk3.isOk + doAssert (not oOk.isErr) + + doAssert oErr.isErr + doAssert oErr2.isErr + doAssert oErr3.isErr + + # Comparisons + doAssert (oOk == oOk) + doAssert (oErr == oErr) + doAssert (oOk != oErr) + + doAssert oOk.get() == oOk.unsafeGet() + oErr.error() + oErr.unsafeError() + + # Mapping + doAssert oOk.map(proc(x: int): string = $x).get() == $oOk.get() + oOk.map(proc(x: int) = discard).get() + + doAssert oOk.mapErr(proc(): int = 10).get() == oOk.get() + doAssert oOk.mapErr(proc() = discard).get() == oOk.get() + + doAssert oErr.mapErr(proc(): int = 10).error() == 10 + + # string conversion + doAssert $oOk == "ok(42)" + doAssert $oErr == "err()" + + proc optQuestion(): OptInt = + let v = ? oOk + ok(v) + + doAssert optQuestion().isOk() + + # Flatten + doAssert Result[OptInt, void].ok(oOk).flatten() == oOk + doAssert Result[OptInt, void].ok(oErr).flatten() == oErr + + # Filter + doAssert oOk.filter(proc(x: int): auto = Result[void, void].ok()) == oOk + doAssert oOk.filter(proc(x: int): auto = Result[void, void].err()).isErr() + doAssert oErr.filter(proc(x: int): auto = Result[void, void].err()) == oErr + + doAssert oOk.filter(proc(x: int): bool = true) == oOk + doAssert oOk.filter(proc(x: int): bool = false).isErr() + doAssert oErr.filter(proc(x: int): bool = true) == oErr + +block: # `cstring` dangling reference protection + type CSRes = Result[void, cstring] + + func cstringF(s: string): CSRes = + when compiles(err(s)): + doAssert false + + discard cstringF("test") + +block: # Experiments + # Can formalise it into a template (https://github.com/arnetheduck/nim-result/issues/8) + template `?=`(v: untyped{nkIdent}, vv: Result): bool = + (let vr = vv; template v: auto {.used.} = unsafeGet(vr); vr.isOk) + + if f ?= Result[int, string].ok(42): + doAssert f == 42 + + # TODO there's a bunch of operators that one could lift through magic - this + # is mainly an example + template `+`(self, other: Result): untyped = + ## Perform `+` on the values of self and other, if both are ok + type R = type(other) + if self.isOk: + if other.isOk: + R.ok(self.value + other.value) + else: + R.err(other.error) + else: + R.err(self.error) + + let rOk = Result[int, string].ok(42) + # Simple lifting.. + doAssert (rOk + rOk)[] == rOk.value + rOk.value + + iterator items[T, E](self: Result[T, E]): T = + ## Iterate over result as if it were a collection of either 0 or 1 items + ## TODO should a Result[seq[X]] iterate over items in seq? there are + ## arguments for and against + if self.isOk: + yield self.value + + # Iteration + var counter2 = 0 + for v in rOk: + counter2 += 1 + + doAssert counter2 == 1, "one-item collection when set" - doAssert a == b