kotlin-result/README.md

333 lines
13 KiB
Markdown
Raw Normal View History

2017-10-21 16:44:55 +00:00
# kotlin-result
[![Maven Central](https://img.shields.io/maven-central/v/com.michael-bull.kotlin-result/kotlin-result.svg)](https://search.maven.org/search?q=g:com.michael-bull.kotlin-result)
2024-03-03 06:16:38 +00:00
[![CI](https://github.com/michaelbull/kotlin-result/actions/workflows/ci.yaml/badge.svg)](https://github.com/michaelbull/kotlin-result/actions/workflows/ci.yaml)
[![License](https://img.shields.io/github/license/michaelbull/kotlin-result.svg)](https://github.com/michaelbull/kotlin-result/blob/master/LICENSE)
![badge][badge-android]
![badge][badge-jvm]
![badge][badge-js]
![badge][badge-nodejs]
![badge][badge-linux]
![badge][badge-windows]
![badge][badge-wasm]
![badge][badge-ios]
![badge][badge-mac]
![badge][badge-tvos]
![badge][badge-watchos]
![badge][badge-js-ir]
![badge][badge-android-native]
![badge][badge-apple-silicon]
2017-10-21 23:59:16 +00:00
2024-03-02 17:10:53 +00:00
A multiplatform Result monad for modelling success or failure operations.
2017-10-21 16:44:55 +00:00
2018-01-11 20:25:59 +00:00
## Installation
```groovy
repositories {
2020-02-12 14:32:53 +00:00
mavenCentral()
2018-01-11 20:25:59 +00:00
}
dependencies {
2024-03-03 00:25:05 +00:00
implementation("com.michael-bull.kotlin-result:kotlin-result:1.1.19")
2018-01-11 20:25:59 +00:00
}
```
## Introduction
2019-12-20 14:51:24 +00:00
The [`Result`][result] monad has two subtypes, [`Ok<V>`][result-ok]
2018-01-11 20:25:59 +00:00
representing success and containing a `value`, and [`Err<E>`][result-err],
2019-12-20 14:51:24 +00:00
representing failure and containing an `error`.
2018-01-11 20:25:59 +00:00
2019-12-20 14:51:24 +00:00
Mappings are available on the [wiki][wiki] to assist those with experience
2018-01-11 20:25:59 +00:00
using the `Result` type in other languages:
- [Elm](https://github.com/michaelbull/kotlin-result/wiki/Elm)
- [Haskell](https://github.com/michaelbull/kotlin-result/wiki/Haskell)
- [Rust](https://github.com/michaelbull/kotlin-result/wiki/Rust)
- [Scala](https://github.com/michaelbull/kotlin-result/wiki/Scala)
2021-01-30 22:26:15 +00:00
## 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][github] if you would like
to include yours.
- [[EN] The Result Monad - Adam Bennett](https://adambennett.dev/2020/05/the-result-monad/)
2021-01-30 22:38:09 +00:00
- [[EN] A Functional Approach to Exception Handling - Tristan Hamilton](https://youtu.be/bEC_t8dH23c?t=132)
2021-01-30 22:26:15 +00:00
- [[EN] kotlin: A functional gold mine - Mark Bucciarelli](http://markbucciarelli.com/posts/2020-01-04_kotlin_functional_gold_mine.html)
- [[EN] Railway Oriented Programming - Scott Wlaschin](https://fsharpforfunandprofit.com/rop/)
- [[JP] KotlinでResult型使うならkotlin-resultを使おう](https://note.com/yasukotelin/n/n6d9e352c344c)
- [[JP] kotlinのコードにReturn Resultを組み込む](https://nnao45.hatenadiary.com/entry/2019/11/30/224820)
2021-01-30 22:31:32 +00:00
## Getting Started
2018-01-11 20:25:59 +00:00
The idiomatic approach to modelling operations that may fail in Railway
2019-12-20 14:51:24 +00:00
Oriented Programming is to avoid throwing an exception and instead make the
2018-01-11 20:25:59 +00:00
return type of your function a `Result`.
```kotlin
fun checkPrivileges(user: User, command: Command): Result<Command, CommandError> {
return if (user.rank >= command.mininimumRank) {
Ok(command)
} else {
Err(CommandError.InsufficientRank(command.name))
}
2018-01-11 20:25:59 +00:00
}
```
To incorporate the `Result` type into an existing codebase that throws
2019-12-20 14:51:24 +00:00
exceptions, you can wrap functions that may `throw` with
[`runCatching`][result-runCatching]. This will execute the block of code and
`catch` any `Throwable`, returning a `Result<T, Throwable>`.
```kotlin
2019-12-20 14:51:24 +00:00
val result: Result<Customer, Throwable> = runCatching {
customerDb.findById(id = 50) // could throw SQLException or similar
}
```
2019-12-20 14:51:24 +00:00
Nullable types, such as the `find` method in the example below, can be
converted to a `Result` using the `toResultOr` extension function.
```kotlin
val result: Result<Customer, String> = customers
.find { it.id == id } // returns Customer?
.toResultOr { "No customer found" }
```
2018-01-11 20:25:59 +00:00
### 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
2019-12-20 14:51:24 +00:00
program error (`UnlockError`) into an exposed client error
2018-01-11 20:25:59 +00:00
(`IncorrectPassword`).
```kotlin
2019-12-20 14:51:24 +00:00
val result: Result<Treasure, UnlockResponse> =
2018-01-11 20:25:59 +00:00
unlockVault("my-password") // returns Result<Treasure, UnlockError>
.mapError { IncorrectPassword } // transform UnlockError into IncorrectPassword
2018-01-11 20:25:59 +00:00
```
### Chaining
Results can be chained to produce a "happy path" of execution. For example, the
2019-12-20 14:51:24 +00:00
happy path for a user entering commands into an administrative console would
2018-01-11 20:25:59 +00:00
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.
2018-01-11 20:25:59 +00:00
```kotlin
tokenize(command.toLowerCase())
.andThen(::findCommand)
2018-01-11 20:25:59 +00:00
.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}") }
)
```
2020-05-11 12:04:50 +00:00
### 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`.
```kotlin
fun functionX(): Result<Int, DomainError> { ... }
fun functionY(): Result<Int, DomainError> { ... }
fun functionZ(): Result<Int, DomainError> { ... }
2020-07-10 19:23:18 +00:00
val sum: Result<Int, DomainError> = binding {
2020-05-11 12:04:50 +00:00
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][bow-bindings], however below is a list of other
resources on the topic of monad comprehensions.
- [Monad comprehensions - Arrow (Kotlin)][arrow-monad-comprehension]
- [Monad comprehensions - Bow (Swift)][bow-monad-comprehension]
- [For comprehensions - Scala][scala-for-comprehension]
#### Coroutine Support
2021-02-10 22:34:44 +00:00
Use of suspending functions within a `binding` block requires an additional
dependency:
```kotlin
dependencies {
2024-03-03 00:25:05 +00:00
implementation("com.michael-bull.kotlin-result:kotlin-result:1.1.19")
implementation("com.michael-bull.kotlin-result:kotlin-result-coroutines:1.1.19")
}
```
**DISCLAIMER:** Supported platforms for the `kotlin-result-coroutines` dependency are
limited to that which coroutines currently supports.
2021-02-10 22:34:44 +00:00
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:
2021-02-10 22:34:44 +00:00
```kotlin
suspend fun failsIn5ms(): Result<Int, DomainErrorA> { ... }
suspend fun failsIn1ms(): Result<Int, DomainErrorB> { ... }
2021-02-10 22:34:44 +00:00
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)
}
```
2017-10-21 16:44:55 +00:00
## Inspiration
Inspiration for this library has been drawn from other languages in which the
Result monad is present, including:
- [Elm](http://package.elm-lang.org/packages/elm-lang/core/latest/Result)
- [Haskell](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Either.html)
- [Rust](https://doc.rust-lang.org/std/result/)
2018-01-11 20:25:59 +00:00
- [Scala](http://www.scala-lang.org/api/2.12.4/scala/util/Either.html)
2017-10-21 16:44:55 +00:00
2017-10-21 18:44:58 +00:00
It also iterates on other Result libraries written in Kotlin, namely:
2017-10-21 16:44:55 +00:00
2017-10-21 18:44:58 +00:00
- [danneu/kotlin-result](https://github.com/danneu/kotlin-result)
- [kittinunf/Result](https://github.com/kittinunf/Result)
- [npryce/result4k](https://github.com/npryce/result4k)
2017-10-21 16:44:55 +00:00
Improvements on the existing solutions include:
2017-10-21 21:33:59 +00:00
- Feature parity with Result types from other languages including Elm, Haskell,
& Rust
2017-10-21 18:44:58 +00:00
- Lax constraints on `value`/`error` nullability
2017-10-21 18:46:14 +00:00
- Lax constraints on the `error` type's inheritance (does not inherit from
2017-10-21 18:46:44 +00:00
`Exception`)
- Top level `Ok` and `Err` classes avoids qualifying usages with
`Result.Ok`/`Result.Err` respectively
2017-10-21 16:44:55 +00:00
- 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`)
2020-09-26 15:53:07 +00:00
- Extensive test suite with almost 100 [unit tests][unit-tests] covering every library method
2017-10-21 18:44:58 +00:00
2018-01-24 13:21:01 +00:00
## Example
The [example][example] module contains an implementation of Scott's
[example application][swalschin-example] that demonstrates the usage of `Result`
in a real world scenario.
It hosts a [ktor][ktor] server on port 9000 with a `/customers` endpoint. The
endpoint responds to both `GET` and `POST` requests with a provided `id`, e.g.
2019-12-20 14:51:24 +00:00
`/customers/100`. Upserting a customer id of 42 is hardcoded to throw an
[`SQLException`][customer-42] to demonstrate how the `Result` type can [map
internal program errors][update-customer-error] to more appropriate
2018-01-24 13:21:01 +00:00
user-facing errors.
### Payloads
2018-01-24 13:23:47 +00:00
#### Fetch customer information
2018-01-24 13:21:01 +00:00
```
2020-08-26 21:02:22 +00:00
$ curl -i -X GET 'http://localhost:9000/customers/1'
2018-01-24 13:21:01 +00:00
```
```
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
2020-08-26 21:02:22 +00:00
Content-Length: 84
2018-01-24 13:21:01 +00:00
{
"firstName": "Michael",
"lastName": "Bull",
2020-08-26 21:02:22 +00:00
"email": "michael@example.com"
2018-01-24 13:21:01 +00:00
}
```
2018-01-24 13:23:47 +00:00
#### Add new customer
2018-01-24 13:21:01 +00:00
```
$ curl -i -X POST \
-H "Content-Type:application/json" \
-d \
'{
"firstName": "Your",
"lastName": "Name",
2020-08-26 21:02:22 +00:00
"email": "email@example.com"
2018-01-24 13:21:01 +00:00
}' \
'http://localhost:9000/customers/200'
```
```
HTTP/1.1 201 Created
Content-Type: text/plain; charset=UTF-8
Content-Length: 16
Customer created
```
2017-10-21 19:03:51 +00:00
## Contributing
Bug reports and pull requests are welcome on [GitHub][github].
## License
This project is available under the terms of the ISC license. See the
[`LICENSE`](LICENSE) file for the copyright information and licensing terms.
2024-03-02 17:13:29 +00:00
[result]: https://github.com/michaelbull/kotlin-result/blob/master/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Result.kt#L10
2020-09-18 05:19:35 +00:00
[result-ok]: https://github.com/michaelbull/kotlin-result/blob/master/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Result.kt#L35
[result-err]: https://github.com/michaelbull/kotlin-result/blob/master/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Result.kt#L58
[result-runCatching]: https://github.com/michaelbull/kotlin-result/blob/master/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Factory.kt#L11
2017-10-21 18:44:58 +00:00
[wiki]: https://github.com/michaelbull/kotlin-result/wiki
2020-09-18 05:19:35 +00:00
[unit-tests]: https://github.com/michaelbull/kotlin-result/tree/master/kotlin-result/src/commonTest/kotlin/com/github/michaelbull/result
2018-01-24 13:21:01 +00:00
[example]: https://github.com/michaelbull/kotlin-result/tree/master/example/src/main/kotlin/com/github/michaelbull/result/example
[swalschin-example]: https://github.com/swlaschin/Railway-Oriented-Programming-Example
[ktor]: http://ktor.io/
2020-09-18 05:19:35 +00:00
[customer-42]: https://github.com/michaelbull/kotlin-result/blob/master/example/src/main/kotlin/com/github/michaelbull/result/example/repository/InMemoryCustomerRepository.kt#L38
2018-01-24 13:24:56 +00:00
[update-customer-error]: https://github.com/michaelbull/kotlin-result/blob/master/example/src/main/kotlin/com/github/michaelbull/result/example/service/CustomerService.kt#L50
2017-10-21 19:03:51 +00:00
[github]: https://github.com/michaelbull/kotlin-result
2020-05-11 12:04:50 +00:00
[bow-bindings]: https://bow-swift.io/docs/patterns/monad-comprehensions/#bindings
[bow-monad-comprehension]: https://bow-swift.io/docs/patterns/monad-comprehensions
[scala-for-comprehension]: https://docs.scala-lang.org/tour/for-comprehensions.html
[arrow-monad-comprehension]: https://arrow-kt.io/docs/0.10/patterns/monad_comprehensions/
[either-syntax]: https://arrow-kt.io/docs/0.10/apidocs/arrow-core-data/arrow.core/-either/#syntax
[badge-android]: http://img.shields.io/badge/-android-6EDB8D.svg?style=flat
[badge-android-native]: http://img.shields.io/badge/support-[AndroidNative]-6EDB8D.svg?style=flat
[badge-jvm]: http://img.shields.io/badge/-jvm-DB413D.svg?style=flat
[badge-js]: http://img.shields.io/badge/-js-F8DB5D.svg?style=flat
[badge-js-ir]: https://img.shields.io/badge/support-[IR]-AAC4E0.svg?style=flat
[badge-nodejs]: https://img.shields.io/badge/-nodejs-68a063.svg?style=flat
[badge-linux]: http://img.shields.io/badge/-linux-2D3F6C.svg?style=flat
[badge-windows]: http://img.shields.io/badge/-windows-4D76CD.svg?style=flat
[badge-wasm]: https://img.shields.io/badge/-wasm-624FE8.svg?style=flat
[badge-apple-silicon]: http://img.shields.io/badge/support-[AppleSilicon]-43BBFF.svg?style=flat
[badge-ios]: http://img.shields.io/badge/-ios-CDCDCD.svg?style=flat
[badge-mac]: http://img.shields.io/badge/-macos-111111.svg?style=flat
[badge-watchos]: http://img.shields.io/badge/-watchos-C0C0C0.svg?style=flat
[badge-tvos]: http://img.shields.io/badge/-tvos-808080.svg?style=flat