Result<V, E> is a monad for modelling success (Ok) or
failure (Err) operations.
repositories {
maven { url = 'https://dl.bintray.com/michaelbull/maven' }
}
dependencies {
compile 'com.michael-bull.kotlin-result:kotlin-result:1.1.4'
}The Result monad has two subtypes, Ok<V>
representing success and containing a value, and Err<E>,
representing failure and containing an error.
Scott Wlaschin's article on Railway Oriented Programming is a great
introduction to the benefits of modelling operations using the Result type.
Mappings are available on the wiki to assist those with experience
using the Result type in other languages:
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" }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 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}") }
)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/errornullability - Lax constraints on the
errortype's inheritance (does not inherit fromException) - Top level
OkandErrclasses 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 over 50 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.
$ curl -i -X GET 'http://localhost:9000/customers/5'
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Content-Length: 93
{
"id": 5,
"firstName": "Michael",
"lastName": "Bull",
"email": "example@email.com"
}
$ curl -i -X POST \
-H "Content-Type:application/json" \
-d \
'{
"firstName": "Your",
"lastName": "Name",
"email": "your@email.com"
}' \
'http://localhost:9000/customers/200'
HTTP/1.1 201 Created
Content-Type: text/plain; charset=UTF-8
Content-Length: 16
Customer created
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.