A multiplatform Result monad for modelling success or failure operations.
Go to file
Michael Bull 87942dd769 Simplify example section in README 2018-01-24 13:23:47 +00:00
example Lift DomainMessage subtypes to top-level declarations in example 2018-01-24 13:03:48 +00:00
gradle/wrapper Update Gradle to 4.4 2017-12-16 19:23:33 +00:00
js Add multi-platform support 2017-12-16 19:30:54 +00:00
jvm Downgrade dokka 2017-12-16 19:50:00 +00:00
src Fix mis-ordered modifier keywords 2018-01-19 14:21:32 +00:00
.editorconfig Simplify editorconfig 2018-01-11 18:29:29 +00:00
.gitignore Add Travis CI configuration file 2017-10-22 01:06:11 +01:00
.travis.yml Add Travis CI configuration file 2017-10-22 01:06:11 +01:00
LICENSE Initial commit 2017-10-21 03:51:30 +01:00
README.md Simplify example section in README 2018-01-24 13:23:47 +00:00
build.gradle Downgrade dokka 2017-12-16 19:50:00 +00:00
gradle.properties Update dependencies 2018-01-17 18:29:33 +00:00
gradlew Update Gradle to v4.3.1 2017-11-28 18:40:25 +00:00
gradlew.bat Initial commit 2017-10-21 03:51:30 +01:00
settings.gradle Add example application 2018-01-11 18:30:41 +00:00

README.md

kotlin-result

Release Build Status License

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 from Exception)
  • Top level Ok and Err classes avoids qualifying usages with Result.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.