kotlin-result
Result<V, E>
is a monad for modelling success (Ok
) or failure (Err
) operations.
Installation
repositories {
mavenCentral()
}
dependencies {
implementation("com.michael-bull.kotlin-result:kotlin-result:1.1.11")
}
Introduction
The Result
monad has two subtypes, Ok<V>
representing success and containing a value
, and Err<E>
, representing failure and containing an error
.
Mappings are available on the wiki to assist those with experience using the Result
type in other languages:
Read More
Below is a collection of videos & articles authored on the subject of this library. Feel free to open a pull request on GitHub if you would like to include yours.
- [EN] The Result Monad - Adam Bennett
- [EN] A Functional Approach to Exception Handling - Tristan Hamilton
- [EN] kotlin: A functional gold mine - Mark Bucciarelli
- [EN] Railway Oriented Programming - Scott Wlaschin
- [JP] KotlinでResult型使うならkotlin-resultを使おう
- [JP] kotlinのコードにReturn Resultを組み込む
Getting Started
The idiomatic approach to modelling operations that may fail in Railway Oriented Programming is to avoid throwing an exception and instead make the return type of your function a Result
.
fun checkPrivileges(user: User, command: Command): Result<Command, CommandError> {
return if (user.rank >= command.mininimumRank) {
Ok(command)
} else {
Err(CommandError.InsufficientRank(command.name))
}
}
To incorporate the Result
type into an existing codebase that throws exceptions, you can wrap functions that may throw
with runCatching
. This will execute the block of code and catch
any Throwable
, returning a Result<T, Throwable>
.
val result: Result<Customer, Throwable> = runCatching {
customerDb.findById(id = 50) // could throw SQLException or similar
}
Nullable types, such as the find
method in the example below, can be converted to a Result
using the toResultOr
extension function.
val result: Result<Customer, String> = customers
.find { it.id == id } // returns Customer?
.toResultOr { "No customer found" }
Transforming Results
Both success and failure results can be transformed within a stage of the railway track. The example below demonstrates how to transform an internal program error (UnlockError
) into an exposed client error (IncorrectPassword
).
val result: Result<Treasure, UnlockResponse> =
unlockVault("my-password") // returns Result<Treasure, UnlockError>
.mapError { IncorrectPassword } // transform UnlockError into IncorrectPassword
Chaining
Results can be chained to produce a "happy path" of execution. For example, the happy path for a user entering commands into an administrative console would consist of: the command being tokenized, the command being registered, the user having sufficient privileges, and the command executing the associated action. The example below uses the checkPrivileges
function we defined earlier.
tokenize(command.toLowerCase())
.andThen(::findCommand)
.andThen { cmd -> checkPrivileges(loggedInUser, cmd) }
.andThen { execute(user = loggedInUser, command = cmd, timestamp = LocalDateTime.now()) }
.mapBoth(
{ output -> printToConsole("returned: $output") },
{ error -> printToConsole("failed to execute, reason: ${error.reason}") }
)
Binding (Monad Comprehension)
The binding
keyword allows multiple calls that each return a Result
to be chained imperatively. When inside a binding
block, the .bind()
function is accessible on any Result
. Each call to bind
will attempt to unwrap the Result
and store its value, returning early if any Result
is an Err
.
In the example below, should functionX()
return an Err
, then execution will skip both functionY()
and functionZ()
, instead storing the Err
from functionX
in the variable named sum
.
fun functionX(): Result<Int, DomainError> { ... }
fun functionY(): Result<Int, DomainError> { ... }
fun functionZ(): Result<Int, DomainError> { ... }
val sum: Result<Int, DomainError> = binding {
val x = functionX().bind()
val y = functionY().bind()
val z = functionZ().bind()
x + y + z
}
println("The sum is $sum") // prints "The sum is Ok(100)"
The binding
keyword primarily draws inspiration from Bow's binding
function, however below is a list of other resources on the topic of monad comprehensions.
Coroutine Support
Use of suspending functions within a binding
block requires an additional dependency:
dependencies {
implementation("com.michael-bull.kotlin-result:kotlin-result:1.1.11")
implementation("com.michael-bull.kotlin-result:kotlin-result-coroutines:1.1.11")
}
The coroutine implementation of binding
has been designed so that the first call to bind()
that fails will cancel all child coroutines within the current coroutine scope.
The example below demonstrates a computationally expensive function that takes five milliseconds to compute being eagerly cancelled as soon as a smaller function fails in just one millisecond:
suspend fun failsIn5ms(): Result<Int, DomainErrorA> { ... }
suspend fun failsIn1ms(): Result<Int, DomainErrorB> { ... }
runBlocking {
val result = binding<Int, BindingError> {
val x = async { failsIn5ms().bind() }
val y = async { failsIn1ms().bind() }
x.await() + y.await()
}
// result will be Err(DomainErrorB)
}
Inspiration
Inspiration for this library has been drawn from other languages in which the Result monad is present, including:
It also iterates on other Result libraries written in Kotlin, namely:
Improvements on the existing solutions include:
- Feature parity with Result types from other languages including Elm, Haskell, & Rust
- Lax constraints on
value
/error
nullability - Lax constraints on the
error
type's inheritance (does not inherit fromException
) - Top level
Ok
andErr
classes avoids qualifying usages withResult.Ok
/Result.Err
respectively - Higher-order functions marked with the
inline
keyword for reduced runtime overhead - Extension functions on
Iterable
&List
for folding, combining, partitioning - Consistent naming with existing Result libraries from other languages (e.g.
map
,mapError
,mapBoth
,mapEither
,and
,andThen
,or
,orElse
,unwrap
) - Extensive test suite with almost 100 unit tests covering every library method
Example
The example module contains an implementation of Scott's example application that demonstrates the usage of Result
in a real world scenario.
It hosts a ktor server on port 9000 with a /customers
endpoint. The endpoint responds to both GET
and POST
requests with a provided id
, e.g. /customers/100
. Upserting a customer id of 42 is hardcoded to throw an SQLException
to demonstrate how the Result
type can map internal program errors to more appropriate user-facing errors.
Payloads
Fetch customer information
$ curl -i -X GET 'http://localhost:9000/customers/1'
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Content-Length: 84
{
"firstName": "Michael",
"lastName": "Bull",
"email": "[email protected]"
}
Add new customer
$ curl -i -X POST \
-H "Content-Type:application/json" \
-d \
'{
"firstName": "Your",
"lastName": "Name",
"email": "[email protected]"
}' \
'http://localhost:9000/customers/200'
HTTP/1.1 201 Created
Content-Type: text/plain; charset=UTF-8
Content-Length: 16
Customer created
Contributing
Bug reports and pull requests are welcome on GitHub.
License
This project is available under the terms of the ISC license. See the LICENSE
file for the copyright information and licensing terms.