-
Notifications
You must be signed in to change notification settings - Fork 0
API
This page documents the Result<Error, Value> API. The API is grouped into sections according to usages.
To showcase the various usages of the API we pretend to request random numbers from an external source. The source of random numbers can be asked for a random number with the following interface.
interface Source {
fun random(): Result<Problem, Int>
}This signals that requesting a random number from the source returns a value of Int but it might fail. The problems that could occur when requesting a random number from our source are:
enum class Problem {
Connection,
Timeout,
Overflow
}When a connection to our source could not be established we signal a Problem.Connection. When a request for a random number takes to long we signal a Problem.Timeout. Finally, when the source returns an number we can not represent as an integer we signal a Problem.Overflow
The code examples are taking from Example.kt, so for more information take a look there.
At the start of a computation chain one needs a to create a result. To create a result one uses the constructor of the sub-classes
Turn a value into a successful result
val result: Result<Problem, Int> = Success(37)data class Success<Error, Value>(val data: Value) : Result<Error, Value>() {
override fun toString(): String {
return "Success(data=$data)"
}
}Turn a problem into a failed result
val result: Result<Problem, Int> = Failure(Problem.Timeout)data class Failure<Error, Value>(val error: Error) : Result<Error, Value>() {
override fun toString(): String {
return "Failure(error=$error)"
}
}Sometimes it is necessary to unwrap a result into an actual value. For example, calling an external library that is unaware of Result<Error, Value>. Because a result could be a failure, one should either provide a default value or, when that is impossible, throw an exception.
Unwrap a result by providing a default value in case of a failure. Delegates to withDefault(producer: (Error) -> Value): Value.
withDefault on a successful result returns the data of the success.
val result: Result<Problem, Int> = Success(37)
result.withDefault(42) shouldBe 37withDefault on a failed result returns the default value.
val result: Result<Problem, Int> = Failure(Problem.Connection)
result.withDefault(42) shouldBe 42fun withDefault(defaultValue: Value): Value {
return withDefault { defaultValue }
}Unwrap a result by producing a default value in case of a failure. The producer of the default value receives the specific problem that occurred and can return a corresponding default value.
When a problem occurs we want to provide the following default values
| Problem | Default Value |
|---|---|
Connection |
-1 |
Timeout |
-2 |
Overflow |
-4 |
The following code demonstrates the behavior for a successful result
val result: Result<Problem, Int> = Success(37)
result.withDefault { problem ->
when (problem) {
Problem.Connection -> -1
Problem.Timeout -> -2
Problem.Overflow -> -4
}
} shouldBe 37The next code demonstrates the behavior for a failed result
val result: Result<Problem, Int> = Failure(Problem.Overflow)
result.withDefault { problem ->
when (problem) {
Problem.Connection -> -1
Problem.Timeout -> -2
Problem.Overflow -> -4
}
} shouldBe -4 fun withDefault(producer: (Error) -> Value): Value {
return when (this) {
is Success -> data
is Failure -> producer(error)
}
}Unwrap a result by throwing an Exception for a failed result. The specific exception to be thrown is determined by an exception producer that receives the problem.
Assuming we have the following custom exception
data class ProblemOccurredException(val problem: Problem): RuntimeException()We see that a successful result returns the actual data
val result: Result<Problem, Int> = Success(37)
result.orThrowException(::ProblemOccurredException) shouldBe 37where a failure would throw an exception instead.
val result: Result<Problem, Int> = Failure(Problem.Overflow)
shouldThrow<ProblemOccurredException> {
result.orThrowException(::ProblemOccurredException)
}fun orThrowException(exceptionProducer: (Error) -> Exception): Value {
return when (this) {
is Success -> data
is Failure -> throw exceptionProducer(error)
}
}The preferred style of working with Result<Error, Value> is to produce a result early in the computation chain, keeping it throughout and use the methods for unwrapping at the end of the computation chain. The following methods help in achieving that goal.
Transform the data of a successful result or keep the failure. To transform the error see mapError(errorTransform: (Error) -> E): Result<E, Value>.
We want our random numbers to be even so we map our result.
val result: Result<Problem, Int> = Success(37)
result.map { 2 * it } shouldBe Success(74)Notice that if a result failed, the failure remains
val result: Result<Problem, Int> = Failure(Problem.Timeout)
result.map {2 * it } shouldBe Failure(Problem.Timeout)fun <T> map(transform: (Value) -> T): Result<Error, T> {
return when (this) {
is Success -> Success(transform(data))
is Failure -> Failure(error)
}
}Transform the error of a failed result or keep the success.
Instead of keeping just our problem, we would like to know when the problem was detected. For that we introduce a TimestampedProblem.
data class TimestampedProblem(val problem: Problem, val timestamp: LocalDateTime = LocalDateTime.now())Now we can map our error. In case of a successful result, the success remains
val result: Result<Problem, Int> = Success(37)
val timestamp = LocalDateTime.now()
result.mapError { problem -> TimestampedProblem(problem, timestamp) } shouldBe Success(37)But a failed result get mapped to a TimestampedProblem.
val result: Result<Problem, Int> = Failure(Problem.Timeout)
val timestamp = LocalDateTime.now()
result.mapError { problem -> TimestampedProblem(problem, timestamp) }
shouldBe Failure(TimestampedProblem(Problem.Timeout, timestamp)))Note that the creation of the timestamp outside of the lambda expression is only done to fascilitate the test. If done properly it should be created inside of the lambda expression.
fun <T> mapError(transform: (Error) -> T): Result<T, Value> {
return when (this) {
is Success -> Success(data)
is Failure -> Failure(transform(error))
}
}The andThen method is the real workhorse among the chaining methods. It allows one to do a thing that might fail, and depending on the result, do another thing that might fail. For example, a request for a customer from the database is made and a Result<DBError, Customer> is received. Without knowing if this call succeeded or failed, one can then make a request for that customers orders, which might fail in it self. The andThen method transparently does the correct unwrapping and wrapping.
These examples are a bit contrived because no real work is done in the chain.
Below we show a table that summarizes the computations in the following code
actual = result.andThen(chain)result |
chain |
actual |
|---|---|---|
Success(37) |
{ n -> Success(n+1) } |
Success(38) |
Success(37) |
{ Failure(Problem.Timeout) } |
Failure(Problem.Timeout) |
Failure(Problem.Connection) |
{ n -> Success(n+1) } |
Failure(Problem.Connection) |
Failure(Problem.Connection) |
{ Failure(Problem.Timeout) } |
Failure(Problem.Connection) |
fun <T> andThen(transform: (Value) -> Result<Error, T>): Result<Error, T> {
return when (this) {
is Success -> transform(data)
is Failure -> Failure(error)
}
}Just like andThen, but for failures. It allows one to do a thing that might fail, and depending on the result, do another thing that might fail. For example, a request for a coupon from the database is made and a Result<DBError, Coupon> is received. In the event that this failed, one can then make a request for a default coupon, which might fail in it self. The andThenError method transparently does the correct unwrapping and wrapping.
These examples are a bit contrived because no real work is done in the chain.
Below we show a table that summarizes the computations in the following code
actual = result.andThen(chain)result |
chain |
actual |
|---|---|---|
Success(37) |
{ Success(51) } |
Success(37) |
Success(37) |
{ Failure(Problem.Timeout) } |
Success(37) |
Failure(Problem.Connection) |
{ Success(51) } |
Success(51) |
Failure(Problem.Connection) |
{ Failure(Problem.Timeout) } |
Failure(Problem.Timeout) |
fun <T> andThen(transform: (Value) -> Result<Error, T>): Result<Error, T> {
return when (this) {
is Success -> transform(data)
is Failure -> Failure(error)
}
}There comes a moment in a chain of computation that you want to use the result without unwrapping the value. For example, when one wants to log various details of the computation chain.
Use the success data if it is present.
To demonstrate the use method we introduced the following class that will use the data of a successful result.
class Receiver<T>(var received: Boolean = false) {
fun receive(n : T) {
received = true
}
}Use of a successful result will send the data to the receiver
val result: Result<Problem, Int> = Success(37)
val receiver = Receiver<Int>()
result.use(receiver::receive)
receiver.received shouldBe trueunless the result is a failure
val result: Result<Problem, Int> = Failure(Problem.Overflow)
val receiver = Receiver<Int>()
result.use(receiver::receive)
receiver.received shouldBe falsefun use(actOn: (Value) -> Unit): Result<Error, Value> {
when (this) {
is Success -> actOn(data)
is Failure -> Unit
}
return this
}The counter part to use(actOn(Value) -> Unit): Result<Error, Value>. Uses the error if it is present.
With the same Receiver as used in the examples of use we can show that when a result is a success, no error is used.
val result: Result<Problem, Int> = Success(37)
val receiver = Receiver<Problem>()
result.useError(receiver::receive)
receiver.received shouldBe falseBut when a result is a failure, the error is received.
val result: Result<Problem, Int> = Failure(Problem.Overflow)
val receiver = Receiver<Problem>()
result.useError(receiver::receive)
receiver.received shouldBe truefun useError(actOn: (Error) -> Unit): Result<Error, Value> {
when (this) {
is Success -> Unit
is Failure -> actOn(error)
}
return this
}