• Stars
    star
    866
  • Rank 52,675 (Top 2 %)
  • Language
    Kotlin
  • License
    ISC License
  • Created about 7 years ago
  • Updated about 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A multiplatform Result monad for modelling success or failure operations.

kotlin-result

Maven Central CI Status License

badge badge badge badge badge badge badge badge badge badge badge badge badge badge

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.18")
}

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.

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.18")
    implementation("com.michael-bull.kotlin-result:kotlin-result-coroutines:1.1.18")
}

DISCLAIMER: Supported platforms for the kotlin-result-coroutines dependency are limited to that which coroutines currently supports.

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 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 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.

More Repositories

1

kotlin-retry

A higher-order function for retrying operations that may fail.
Kotlin
301
star
2

kotlin-inline-logger

A logger facilitating lazily-evaluated log calls via Kotlin's inline classes & functions.
Kotlin
83
star
3

aurelia-hacker-news

A recreation of the Hacker News website written in TypeScript and built with Aurelia.
TypeScript
60
star
4

zoom.ts

A lightweight TypeScript library for image zooming, as seen on Medium.
TypeScript
48
star
5

material-bottom-nav

A bottom navigation bar adhering to the Material Design specification.
SCSS
46
star
6

kotlin-coroutines-jdbc

A library for interacting with blocking JDBC drivers using Kotlin Coroutines.
Kotlin
41
star
7

rs-api

An open-source implementation of a web-service client, written in Java, that allows interaction with the various APIs available for the popular MMORPG; RuneScape.
Java
33
star
8

vlc-credit-skipper

Automatically skip intro/outro credit sequences in VLC.
Lua
31
star
9

aurelia-typescript-webpack-starter

A minimal Aurelia starter kit written in TypeScript and built using webpack.
TypeScript
27
star
10

c-dictionary-trie

A dictionary implementation that is written in C and backed by a prefix tree.
C
24
star
11

loona-intro-generator

Create your own animated intro sequence based on the LOONA music videos.
SCSS
19
star
12

beancount-plugins

A collection of my custom beancount importers & price sources, written in Python
Python
15
star
13

aurelia-swipeout

A custom element for iOS style swipeout actions, backed by Hammer.js
TypeScript
13
star
14

aurelia-split-pane

A custom element for resizable split panes.
TypeScript
12
star
15

spring-boot-starter-recaptcha

Spring Boot starter for Google's reCAPTCHA v3.
Kotlin
8
star
16

svg-stockpile

An optimizing and stacking tool for Scalable Vector Graphics, written in Java.
Java
6
star
17

ModBot

Automated moderation for VBulletin based forums using the VBulletin Mobile API.
Java
5
star
18

kotlin-quadtree

A quadtree implemented in Kotlin.
Kotlin
5
star
19

wysiwyg-editor

A “what-you-see-is-what-you-get” editor built on React & Slate.
JavaScript
5
star
20

advent-2021

Solutions for Advent of Code 2021, written in Kotlin.
Kotlin
4
star
21

spring-boot-deployment-demo

Java
4
star
22

react-gsi

React bindings for the 'Sign in With Google for Web' API
TypeScript
4
star
23

spring-boot-sitemap-demo

Java
3
star
24

aurelia-ssr-starter

A minimal Aurelia starter kit supporting server side rendering.
TypeScript
3
star
25

advent-2019

Solutions for Advent of Code 2019
Kotlin
2
star
26

karma-fail-fast-reporter

A Karma plugin. Report failures as soon as they occur.
JavaScript
2
star
27

m2m-sms

M2M to SMS is a website platform written in PHP that facilitates the transit of Machine-to-Machine device messages, formatted as Short Message Service messages, over the Simple Object Access Protocol via the EE M2M Connect Service.
PHP
2
star
28

advent-2023

Solutions for Advent of Code 2023, written in Kotlin.
Kotlin
1
star