A multiplatform Result monad for modelling success or failure operations.
repositories {
mavenCentral()
}
dependencies {
implementation("com.michael-bull.kotlin-result:kotlin-result:2.1.0")
}In functional programming, the result Result type is a monadic type
holding a returned value or an error.
To indicate an operation that succeeded, return an Ok(value)
with the successful value. If it failed, return an Err(error)
with the error that caused the failure.
This helps to define a clear happy/unhappy path of execution that is commonly referred to as Railway Oriented Programming, whereby the happy and unhappy paths are represented as separate railways.
The Result type is modelled as an
inline value class. This achieves zero object
allocations on the happy path.
A full breakdown, with example output Java code, is available in the Overhead design doc.
kotlin-result targets all three tiers outlined by the
Kotlin/Native target support
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を組み込む
- [JP] kotlin-resultを半年使ってみて
- [JP] kotlin-result入門
Mappings are available on the wiki to assist those with experience
using the Result type in other languages:
Below is a simple example of how you may use the Result type to model a
function that may fail.
fun checkPrivileges(user: User, command: Command): Result<Command, CommandError> {
return if (user.rank >= command.minimumRank) {
Ok(command)
} else {
Err(CommandError.InsufficientRank(command.name))
}
}When interacting with code outside your control that may throw exceptions, wrap
the call with runCatching to capture its execution as 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" }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 the exposed client error IncorrectPassword.
val result: Result<Treasure, UnlockResponse> =
unlockVault("my-password") // returns Result<Treasure, UnlockError>
.mapError { IncorrectPassword } // transform UnlockError into IncorrectPasswordResults 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}") }
)The binding function 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 error.
In the example below, should functionX() return an error, then execution will
skip both functionY() and functionZ(), instead storing the error from
functionX in the variable named sum.
fun functionX(): Result<Int, SumError> { ... }
fun functionY(): Result<Int, SumError> { ... }
fun functionZ(): Result<Int, SumError> { ... }
val sum: Result<Int, SumError> = 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 function primarily draws inspiration from
Bow's binding function, however below is a list of other
resources on the topic of monad comprehensions.
Use of suspending functions within a coroutineBinding block requires an
additional dependency:
dependencies {
implementation("com.michael-bull.kotlin-result:kotlin-result:2.1.0")
implementation("com.michael-bull.kotlin-result:kotlin-result-coroutines:2.1.0")
}The coroutineBinding function runs inside a
coroutineScope, facilitating concurrent
decomposition of work.
When any call to bind() inside the block fails, the scope fails, cancelling
all other children.
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: Result<Int, BindingError> = coroutineBinding { // this creates a new CoroutineScope
val x = async { failsIn5ms().bind() }
val y = async { failsIn1ms().bind() }
x.await() + y.await()
}
// result will be Err(DomainErrorB)
}Inspiration for this library has been drawn from other languages in which the Result monad is present, including:
Improvements on existing solutions such the stdlib include:
- Reduced runtime overhead with zero object allocations on the happy path
- Feature parity with Result types from other languages including Elm, Haskell, & Rust
- Lax constraints on
value/errornullability - Lax constraints on the
errortype's inheritance (does not inherit fromException) - Top level
OkandErrfunctions avoids qualifying usages withResult.Ok/Result.Errrespectively - Higher-order functions marked with the
inlinekeyword for reduced runtime overhead - Extension functions on
Iterable&Listfor 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
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.
Bug reports and pull requests are welcome on GitHub.
This project is available under the terms of the ISC license. See the
LICENSE file for the copyright information and licensing terms.