Skip to content
This repository was archived by the owner on Sep 10, 2021. It is now read-only.
Daan van Berkel edited this page Feb 2, 2021 · 17 revisions

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.

Creation

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

Success<Error, Value>(val data: Value)

Turn a value into a successful result

Example

val result: Result<Problem, Int> = Success(37)

Definition

data class Success<Error, Value>(val data: Value) : Result<Error, Value>() {
    override fun toString(): String {
        return "Success(data=$data)"
    }
}

Failure<Error, Value>(val error: Error)

Turn a problem into a failed result

Example

val result: Result<Problem, Int> = Failure(Problem.Timeout)

Definition

data class Failure<Error, Value>(val error: Error) : Result<Error, Value>() {
    override fun toString(): String {
        return "Failure(error=$error)"
    }
}

Unwrapping

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.

withDefault(defaultValue: Value): Value

Unwrap a result by providing a default value in case of a failure. Delegates to withDefault(producer: (Error) -> Value): Value.

Examples

withDefault on a successful result returns the data of the success.

val result: Result<Problem, Int> = Success(37)

result.withDefault(42) shouldBe 37

withDefault on a failed result returns the default value.

val result: Result<Problem, Int> = Failure(Problem.Connection)

result.withDefault(42) shouldBe 42

Definition

fun withDefault(defaultValue: Value): Value {
    return withDefault { defaultValue }
}

withDefault(producer: (Error) -> Value): Value

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.

Examples

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 37

The 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                                                     

Definition

fun withDefault(producer: (Error) -> Value): Value {
    return when (this) {
        is Success -> data
        is Failure -> producer(error)
    }
}

orThrowException(exceptionProducer: (Error) -> Exception): Value

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.

Examples

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 37

where a failure would throw an exception instead.

val result: Result<Problem, Int> = Failure(Problem.Overflow)

shouldThrow<ProblemOccurredException> {
    result.orThrowException(::ProblemOccurredException)
}

Definition

fun orThrowException(exceptionProducer: (Error) -> Exception): Value {
    return when (this) {
        is Success -> data
        is Failure -> throw exceptionProducer(error)
    }
}

Chaining

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.

map(transform: (Value) -> T)

Transform the data of a successful result or keep the failure. To transform the error see mapError(errorTransform: (Error) -> E): Result<E, Value>.

Examples

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)

Definition

fun <T> map(transform: (Value) -> T): Result<Error, T> {
    return when (this) {
        is Success -> Success(transform(data))
        is Failure -> Failure(error)
    }
}

mapError(transform: (Error) -> E): Result<E, Value>

Transform the error of a failed result or keep the success.

Examples

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.

Definition

fun <T> mapError(transform: (Error) -> T): Result<T, Value> {
    return when (this) {
        is Success -> Success(data)
        is Failure -> Failure(transform(error))
    }
}

andThen(chain: (Value) -> Result<Error, T>): Result<Error, T>

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.

Examples

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)

Definition

fun <T> andThen(transform: (Value) -> Result<Error, T>): Result<Error, T> {
    return when (this) {
        is Success -> transform(data)
        is Failure -> Failure(error)
    }
}

andThenError(chain: (Error) -> Result<T, Value>): Result<T, Value>

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.

Examples

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)

Definition

fun <T> andThen(transform: (Value) -> Result<Error, T>): Result<Error, T> {
    return when (this) {
        is Success -> transform(data)
        is Failure -> Failure(error)
    }
}

Using

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(actOn: (Value) -> Unit): Result<Error, Value>

Use the success data if it is present.

Examples

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 true

unless the result is a failure

val result: Result<Problem, Int> = Failure(Problem.Overflow)
val receiver = Receiver<Int>()

result.use(receiver::receive)

receiver.received shouldBe false

Definition

fun use(actOn: (Value) -> Unit): Result<Error, Value> {
    when (this) {
        is Success -> actOn(data)
        is Failure -> Unit
    }
    return this
}

useError(actOn: (Error) -> Unit): Result<Error, Value>

The counter part to use(actOn(Value) -> Unit): Result<Error, Value>. Uses the error if it is present.

Examples

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 false

But 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 true

Definition

fun useError(actOn: (Error) -> Unit): Result<Error, Value> {
    when (this) {
        is Success -> Unit
        is Failure -> actOn(error)
    }
    return this
}