example | ||
gradle/wrapper | ||
js | ||
jvm | ||
src | ||
.editorconfig | ||
.gitignore | ||
.travis.yml | ||
build.gradle | ||
gradle.properties | ||
gradlew | ||
gradlew.bat | ||
LICENSE | ||
README.md | ||
settings.gradle |
kotlin-result
Result<V, E>
is a monad for modelling success (Ok
) or
failure (Err
) operations.
Installation
repositories {
maven { url "https://jitpack.io" }
}
dependencies {
compile 'com.github.michaelbull:kotlin-result:1.0.7'
}
Introduction
The Result
monad has two subtypes, Ok<V>
representing success and containing a value
, and Err<E>
,
representing an error and containing an error
value.
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:
Creating Results
To begin incorporating the Result
type into an existing codebase, you can
wrap functions that may fail (i.e. throw an Exception
) with
Result.of
. This will execute the block of code and catch
any
Exception
, returning a Result<T, Exception>
.
val result: Result<Customer, Exception> = Result.of {
customerDb.findById(id = 50) // could throw SQLException or similar
}
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 findById(id: Int): Result<Customer, DatabaseError> {
val customer = getAllCustomers().find { it.id == id }
return if (customer != null) Ok(customer) else Err(DatabaseError.CustomerNotFound)
}
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 { UnlockResponse.IncorrectPassword } // transform UnlockError into UnlockResponse.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.
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}") }
)
Each of the andThen
steps produces its own result, for example:
fun checkPrivileges(user: User, command: TokenizedCommand): Result<Command, CommandError> {
return when {
user.rank >= command.minRank -> Ok(command)
else -> Err(CommandError.InsufficientRank(command.tokens.name))
}
}
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
- Multiplatform project support
- 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 over 50 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/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"
}
Add new customer
$ 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
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.