From 60769f9983e1de24f71dd67917d2fbb169df74ac Mon Sep 17 00:00:00 2001 From: Michael Bull Date: Sat, 16 Mar 2024 21:25:31 +0000 Subject: [PATCH 1/4] Bump version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 697dbd3..bb9d31d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=com.michael-bull.kotlin-result -version=1.1.22-SNAPSHOT +version=2.0.0-SNAPSHOT description=A multiplatform Result monad for modelling success or failure operations. kotlin.code.style=official From eecd1b7d9946f541af3f5d453905024ff97e7c26 Mon Sep 17 00:00:00 2001 From: Michael Bull Date: Tue, 5 Mar 2024 15:16:31 +0000 Subject: [PATCH 2/4] Remove deprecated functions --- .../coroutines/binding/SuspendableBinding.kt | 22 --------- .../com/github/michaelbull/result/And.kt | 9 ---- .../com/github/michaelbull/result/Binding.kt | 6 --- .../com/github/michaelbull/result/Get.kt | 18 -------- .../com/github/michaelbull/result/Iterable.kt | 45 ------------------- .../com/github/michaelbull/result/Or.kt | 9 ---- .../com/github/michaelbull/result/Result.kt | 16 ------- .../com/github/michaelbull/result/Unwrap.kt | 18 -------- .../result/coroutines/SuspendableBinding.kt | 32 ------------- 9 files changed, 175 deletions(-) delete mode 100644 kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/binding/SuspendableBinding.kt delete mode 100644 kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/SuspendableBinding.kt diff --git a/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/binding/SuspendableBinding.kt b/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/binding/SuspendableBinding.kt deleted file mode 100644 index ed61b42..0000000 --- a/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/binding/SuspendableBinding.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.github.michaelbull.result.coroutines.binding - -import com.github.michaelbull.result.Result -import com.github.michaelbull.result.coroutines.CoroutineBindingScope -import com.github.michaelbull.result.coroutines.coroutineBinding - -@Deprecated( - message = "Use coroutineBinding instead", - replaceWith = ReplaceWith( - expression = "coroutineBinding(block)", - imports = ["com.github.michaelbull.result.coroutines.coroutineBinding"] - ) -) -public suspend inline fun binding(crossinline block: suspend CoroutineBindingScope.() -> V): Result { - return coroutineBinding(block) -} - -@Deprecated( - message = "Use CoroutineBindingScope instead", - replaceWith = ReplaceWith("CoroutineBindingScope") -) -public typealias SuspendableResultBinding = CoroutineBindingScope diff --git a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/And.kt b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/And.kt index 1acfb30..b1fb391 100644 --- a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/And.kt +++ b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/And.kt @@ -15,15 +15,6 @@ public infix fun Result.and(result: Result): Result } } -@Deprecated("Use andThen instead", ReplaceWith("andThen { result() }")) -public inline infix fun Result.and(result: () -> Result): Result { - contract { - callsInPlace(result, InvocationKind.AT_MOST_ONCE) - } - - return andThen { result() } -} - /** * Maps this [Result][Result] to [Result][Result] by either applying the [transform] * function if this result [is ok][Result.isOk], or returning [this]. diff --git a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Binding.kt b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Binding.kt index 5202043..b2e4533 100644 --- a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Binding.kt +++ b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Binding.kt @@ -41,12 +41,6 @@ public inline fun binding(crossinline block: BindingScope.() -> V): Re internal expect object BindException : Exception -@Deprecated( - message = "Use BindingScope instead", - replaceWith = ReplaceWith("BindingScope") -) -public typealias ResultBinding = BindingScope - public interface BindingScope { public fun Result.bind(): V } diff --git a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Get.kt b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Get.kt index f9a395b..815b8e6 100644 --- a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Get.kt +++ b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Get.kt @@ -55,15 +55,6 @@ public infix fun Result.getOr(default: V): V { } } -@Deprecated("Use getOrElse instead", ReplaceWith("getOrElse { default() }")) -public inline infix fun Result.getOr(default: () -> V): V { - contract { - callsInPlace(default, InvocationKind.AT_MOST_ONCE) - } - - return getOrElse { default() } -} - /** * Returns the [error][Result.error] if this result [is an error][Result.isErr], otherwise * [default]. @@ -80,15 +71,6 @@ public infix fun Result.getErrorOr(default: E): E { } } -@Deprecated("Use getOrElse instead", ReplaceWith("getErrorOrElse { default() }")) -public inline infix fun Result.getErrorOr(default: () -> E): E { - contract { - callsInPlace(default, InvocationKind.AT_MOST_ONCE) - } - - return getErrorOrElse { default() } -} - /** * Returns the [value][Result.value] if this result [is ok][Result.isOk], otherwise the * [transformation][transform] of the [error][Result.error]. diff --git a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Iterable.kt b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Iterable.kt index bd0b95b..c1c86a0 100644 --- a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Iterable.kt +++ b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Iterable.kt @@ -170,29 +170,6 @@ public fun > valuesOf(vararg results: R): List { return results.asIterable().filterValues() } -@Deprecated( - message = "Use allValuesOf instead", - replaceWith = ReplaceWith("valuesOf(results)") -) -public fun > getAll(vararg results: R): List { - return results.asIterable().filterValues() -} - - -/** - * Extracts from an [Iterable] of [Results][Result] all the [Ok] elements. All the [Ok] elements - * are extracted in order. - * - * - Haskell: [Data.Either.lefts](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Either.html#v:lefts) - */ -@Deprecated( - message = "Use filterValues instead", - replaceWith = ReplaceWith("filterValues()") -) -public fun Iterable>.getAll(): List { - return filterValues() -} - /** * Returns a [List] containing the [error][Result.error] of each element in the specified [results] * that [is an error][Result.isErr]. Elements in the returned list are in the same order as the @@ -204,28 +181,6 @@ public fun > errorsOf(vararg results: R): List { return results.asIterable().filterErrors() } -@Deprecated( - message = "Use errorsOf instead", - replaceWith = ReplaceWith("errorsOf(results)") -) -public fun > getAllErrors(vararg results: R): List { - return results.asIterable().filterErrors() -} - -/** - * Extracts from an [Iterable] of [Results][Result] all the [Err] elements. All the [Err] elements - * are extracted in order. - * - * - Haskell: [Data.Either.rights](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Either.html#v:rights) - */ -@Deprecated( - message = "Use filterErrors instead", - replaceWith = ReplaceWith("filterErrors()") -) -public fun Iterable>.getAllErrors(): List { - return filterErrors() -} - /** * Partitions the specified [results] into a [Pair] of [Lists][List]. An element that * [is ok][Result.isOk] will appear in the [first][Pair.first] list, whereas an element that diff --git a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Or.kt b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Or.kt index c0f20b9..2b993fd 100644 --- a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Or.kt +++ b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Or.kt @@ -15,15 +15,6 @@ public infix fun Result.or(result: Result): Result { } } -@Deprecated("Use orElse instead", ReplaceWith("orElse { result() }")) -public inline infix fun Result.or(result: () -> Result): Result { - contract { - callsInPlace(result, InvocationKind.AT_MOST_ONCE) - } - - return orElse { result() } -} - /** * Returns the [transformation][transform] of the [error][Result.error] if this result * [is an error][Result.isErr], otherwise [this]. diff --git a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Result.kt b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Result.kt index 3eb5c53..9cb3cfa 100644 --- a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Result.kt +++ b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Result.kt @@ -51,22 +51,6 @@ public sealed class Result { public abstract operator fun component1(): V? public abstract operator fun component2(): E? - - public companion object { - - /** - * Invokes a [function] and wraps it in a [Result], returning an [Err] - * if an [Exception] was thrown, otherwise [Ok]. - */ - @Deprecated("Use top-level runCatching instead", ReplaceWith("runCatching(function)")) - public inline fun of(function: () -> V): Result { - return try { - Ok(function.invoke()) - } catch (ex: Exception) { - Err(ex) - } - } - } } /** diff --git a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Unwrap.kt b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Unwrap.kt index 9c5d42e..136b65f 100644 --- a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Unwrap.kt +++ b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Unwrap.kt @@ -24,15 +24,6 @@ public fun Result.unwrap(): V { } } -@Deprecated("Use lazy-evaluating variant instead", ReplaceWith("expect { message }")) -public infix fun Result.expect(message: String): V { - contract { - returns() implies (this@expect is Ok) - } - - return expect { message } -} - /** * Returns the [value][Result.value] if this result [is ok][Result.isOk], otherwise throws an * [UnwrapException] with the specified [message]. @@ -75,15 +66,6 @@ public fun Result.unwrapError(): E { } } -@Deprecated("Use lazy-evaluating variant instead", ReplaceWith("expectError { message }")) -public infix fun Result.expectError(message: String): E { - contract { - returns() implies (this@expectError is Err) - } - - return expectError { message } -} - /** * Returns the [error][Result.error] if this result [is an error][Result.isErr], otherwise throws * an [UnwrapException] with the specified [message]. diff --git a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/SuspendableBinding.kt b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/SuspendableBinding.kt deleted file mode 100644 index 93a9c02..0000000 --- a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/SuspendableBinding.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.michaelbull.result.coroutines - -import com.github.michaelbull.result.BindException -import com.github.michaelbull.result.BindingScope -import com.github.michaelbull.result.BindingScopeImpl -import com.github.michaelbull.result.Ok -import com.github.michaelbull.result.Result -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract - -/** - * Suspending variant of [binding][com.github.michaelbull.result.binding]. - */ -@Deprecated( - message = "Will throw a runtime exception if used with async requests that fail to bind. " + - "See https://github.com/michaelbull/kotlin-result/pull/28 " + - "Please import the kotlin-result-coroutines library to continue using this feature.", - level = DeprecationLevel.WARNING -) -public suspend inline fun binding(crossinline block: suspend BindingScope.() -> V): Result { - contract { - callsInPlace(block, InvocationKind.EXACTLY_ONCE) - } - - return with(BindingScopeImpl()) { - try { - Ok(block()) - } catch (ex: BindException) { - result!! - } - } -} From 981fbe2812185f5083f44854409601bb42fcfb3c Mon Sep 17 00:00:00 2001 From: Michael Bull Date: Tue, 5 Mar 2024 20:36:03 +0000 Subject: [PATCH 3/4] Convert Result to an inline value class --- .../com/github/michaelbull/result/Get.kt | 15 --- .../com/github/michaelbull/result/Result.kt | 126 +++++++----------- .../com/github/michaelbull/result/Unwrap.kt | 10 -- 3 files changed, 50 insertions(+), 101 deletions(-) diff --git a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Get.kt b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Get.kt index 815b8e6..535d7ad 100644 --- a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Get.kt +++ b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Get.kt @@ -10,11 +10,6 @@ import kotlin.contracts.contract * - Rust: [Result.ok](https://doc.rust-lang.org/std/result/enum.Result.html#method.ok) */ public fun Result.get(): V? { - contract { - returnsNotNull() implies (this@get is Ok) - returns(null) implies (this@get is Err) - } - return when { isOk -> value else -> null @@ -27,11 +22,6 @@ public fun Result.get(): V? { * - Rust: [Result.err](https://doc.rust-lang.org/std/result/enum.Result.html#method.err) */ public fun Result.getError(): E? { - contract { - returns(null) implies (this@getError is Ok) - returnsNotNull() implies (this@getError is Err) - } - return when { isErr -> error else -> null @@ -111,10 +101,6 @@ public inline infix fun Result.getErrorOrElse(transform: (V) -> E): * This is functionally equivalent to [`getOrElse { throw it }`][getOrElse]. */ public fun Result.getOrThrow(): V { - contract { - returns() implies (this@getOrThrow is Ok) - } - return when { isOk -> value else -> throw error @@ -127,7 +113,6 @@ public fun Result.getOrThrow(): V { */ public inline infix fun Result.getOrThrow(transform: (E) -> Throwable): V { contract { - returns() implies (this@getOrThrow is Ok) callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } diff --git a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Result.kt b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Result.kt index 9cb3cfa..804c926 100644 --- a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Result.kt +++ b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Result.kt @@ -1,19 +1,21 @@ package com.github.michaelbull.result +import kotlin.jvm.JvmInline + /** * Returns a [Result] that [is ok][Result.isOk] and contains a [value][Result.value]. */ -@Suppress("FunctionName", "DEPRECATION") +@Suppress("FunctionName") public fun Ok(value: V): Result { - return Ok(value, null) + return Result(value) } /** * Returns a [Result] that [is an error][Result.isErr] and contains an [error][Result.error]. */ -@Suppress("FunctionName", "DEPRECATION") +@Suppress("FunctionName") public fun Err(error: E): Result { - return Err(error, null) + return Result(Failure(error)) } /** @@ -37,94 +39,66 @@ public inline fun Result.asErr(): Result { /** * [Result] is a type that represents either success ([Ok]) or failure ([Err]). * + * A [Result] that [is ok][Result.isOk] will have a [value][Result.value] of type [V], whereas a + * [Result] that [is an error][Result.isErr] will have an [error][Result.error] of type [E]. + * * - Elm: [Result](http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Result) * - Haskell: [Data.Either](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Either.html) * - Rust: [Result](https://doc.rust-lang.org/std/result/enum.Result.html) */ -public sealed class Result { +@JvmInline +public value class Result internal constructor( + private val inlineValue: Any?, +) { - public abstract val value: V - public abstract val error: E + @Suppress("UNCHECKED_CAST") + public val value: V + get() = inlineValue as V - public abstract val isOk: Boolean - public abstract val isErr: Boolean - - public abstract operator fun component1(): V? - public abstract operator fun component2(): E? -} + @Suppress("UNCHECKED_CAST") + public val error: E + get() = (inlineValue as Failure).error -/** - * Represents a successful [Result], containing a [value]. - */ -@Deprecated( - message = "Using Ok as a return type is deprecated.", - replaceWith = ReplaceWith("Result"), -) -public class Ok internal constructor( - override val value: V, - @Suppress("UNUSED_PARAMETER") placeholder: Any?, -) : Result() { - - override val error: Nothing - get() { - throw NoSuchElementException() - } + public val isOk: Boolean + get() = inlineValue !is Failure<*> - override val isOk: Boolean = true - override val isErr: Boolean = false + public val isErr: Boolean + get() = inlineValue is Failure<*> - override fun component1(): V = value - override fun component2(): Nothing? = null - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as Ok<*> - - if (value != other.value) return false - - return true + public operator fun component1(): V? { + return when { + isOk -> value + else -> null + } } - override fun hashCode(): Int = value.hashCode() - override fun toString(): String = "Ok($value)" -} - -/** - * Represents a failed [Result], containing an [error]. - */ -@Deprecated( - message = "Using Err as a return type is deprecated.", - replaceWith = ReplaceWith("Result"), -) -public class Err internal constructor( - override val error: E, - @Suppress("UNUSED_PARAMETER") placeholder: Any?, -) : Result() { - - override val value: Nothing - get() { - throw NoSuchElementException() + public operator fun component2(): E? { + return when { + isErr -> error + else -> null } + } - override val isOk: Boolean = false - override val isErr: Boolean = true - - override fun component1(): Nothing? = null - override fun component2(): E = error + override fun toString(): String { + return when { + isOk -> "Ok($value)" + else -> "Err($error)" + } + } +} +private class Failure( + val error: E, +) { override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as Err<*> - - if (error != other.error) return false + return other is Failure<*> && error == other.error + } - return true + override fun hashCode(): Int { + return error.hashCode() } - override fun hashCode(): Int = error.hashCode() - override fun toString(): String = "Err($error)" + override fun toString(): String { + return "Failure($error)" + } } diff --git a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Unwrap.kt b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Unwrap.kt index 136b65f..b72389c 100644 --- a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Unwrap.kt +++ b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Unwrap.kt @@ -14,10 +14,6 @@ public class UnwrapException(message: String) : Exception(message) * @throws UnwrapException if this result [is an error][Result.isErr]. */ public fun Result.unwrap(): V { - contract { - returns() implies (this@unwrap is Ok) - } - return when { isOk -> value else -> throw UnwrapException("called Result.unwrap on an Err value $error") @@ -38,7 +34,6 @@ public fun Result.unwrap(): V { public inline infix fun Result.expect(message: () -> Any): V { contract { callsInPlace(message, InvocationKind.AT_MOST_ONCE) - returns() implies (this@expect is Ok) } return when { @@ -56,10 +51,6 @@ public inline infix fun Result.expect(message: () -> Any): V { * @throws UnwrapException if this result [is ok][Result.isOk]. */ public fun Result.unwrapError(): E { - contract { - returns() implies (this@unwrapError is Err) - } - return when { isErr -> error else -> throw UnwrapException("called Result.unwrapError on an Ok value $value") @@ -80,7 +71,6 @@ public fun Result.unwrapError(): E { public inline infix fun Result.expectError(message: () -> Any): E { contract { callsInPlace(message, InvocationKind.AT_MOST_ONCE) - returns() implies (this@expectError is Err) } return when { From fb3ef3dc66f08eb3dfeea41a5eb0f6bc2eb8d6d4 Mon Sep 17 00:00:00 2001 From: Michael Bull Date: Sat, 16 Mar 2024 20:11:19 +0000 Subject: [PATCH 4/4] Update README --- README.md | 182 ++++++++++++++++++++++++------------------------------ 1 file changed, 81 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index eb1f322..84a7da0 100644 --- a/README.md +++ b/README.md @@ -35,19 +35,32 @@ dependencies { ## Introduction -The [`Result`][result] monad has two subtypes, [`Ok`][result-ok] -representing success and containing a `value`, and [`Err`][result-err], -representing failure and containing an `error`. +In functional programming, the result [`Result`][result] type is a monadic type +holding a returned [value][result-value] or an [error][result-error]. -Mappings are available on the [wiki][wiki] to assist those with experience -using the `Result` type in other languages: +To indicate an operation that succeeded, return an [`Ok(value)`][result-Ok] +with the successful `value`. If it failed, return an [`Err(error)`][result-Err] +with the `error` that caused the failure. -- [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) +This helps to define a clear happy/unhappy path of execution that is commonly +referred to as [Railway Oriented Programming][rop], whereby the happy and +unhappy paths are represented as separate railways. -## Read More +### Overhead + +The `Result` type is modelled as an +[inline value class][kotlin-inline-classes]. This achieves zero object +allocations on the happy path. + +A full breakdown, with example output Java code, is available in the +[Overhead][wiki-Overhead] design doc. + +### Multiplatform Support + +`kotlin-result` targets all three tiers outlined by the +[Kotlin/Native target support][kotlin-native-target-support] + +### 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 @@ -60,11 +73,18 @@ to include yours. - [[JP] KotlinでResult型使うならkotlin-resultを使おう](https://note.com/yasukotelin/n/n6d9e352c344c) - [[JP] kotlinのコードにReturn Resultを組み込む](https://nnao45.hatenadiary.com/entry/2019/11/30/224820) +Mappings are available on the [wiki][wiki] to assist those with experience +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) + ## 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`. +Below is a simple example of how you may use the `Result` type to model a +function that may fail. ```kotlin fun checkPrivileges(user: User, command: Command): Result { @@ -76,10 +96,9 @@ fun checkPrivileges(user: User, command: Command): Result } ``` -To incorporate the `Result` type into an existing codebase that throws -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`. +When interacting with code outside your control that may throw exceptions, wrap +the call with [`runCatching`][result-runCatching] to capture its execution as a +`Result`: ```kotlin val result: Result = runCatching { @@ -100,8 +119,7 @@ val result: Result = customers 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`). +program error `UnlockError` into the exposed client error `IncorrectPassword`. ```kotlin val result: Result = @@ -130,21 +148,22 @@ tokenize(command.toLowerCase()) ### 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`. +The [`binding`][result-binding] function 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 error. -In the example below, should `functionX()` return an `Err`, then execution will -skip both `functionY()` and `functionZ()`, instead storing the `Err` from +In the example below, should `functionX()` return an error, then execution will +skip both `functionY()` and `functionZ()`, instead storing the error from `functionX` in the variable named `sum`. ```kotlin -fun functionX(): Result { ... } -fun functionY(): Result { ... } -fun functionZ(): Result { ... } +fun functionX(): Result { ... } +fun functionY(): Result { ... } +fun functionZ(): Result { ... } -val sum: Result = binding { +val sum: Result = binding { val x = functionX().bind() val y = functionY().bind() val z = functionZ().bind() @@ -154,18 +173,18 @@ val sum: Result = binding { println("The sum is $sum") // prints "The sum is Ok(100)" ``` -The `binding` keyword primarily draws inspiration from +The `binding` function 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] +- [Monad comprehensions - Arrow (Kotlin)](https://arrow-kt.io/docs/0.10/patterns/monad_comprehensions/) +- [Monad comprehensions - Bow (Swift)](https://bow-swift.io/docs/patterns/monad-comprehensions) +- [For comprehensions - Scala](https://docs.scala-lang.org/tour/for-comprehensions.html) -#### Coroutine Support +#### Coroutine Binding Support -Use of suspending functions within a `binding` block requires an additional -dependency: +Use of suspending functions within a `coroutineBinding` block requires an +additional dependency: ```kotlin dependencies { @@ -174,9 +193,12 @@ dependencies { } ``` -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 [`coroutineBinding`][result-coroutineBinding] function runs inside a +[`coroutineScope`][kotlin-coroutineScope], facilitating _concurrent +decomposition of work_. + +When any call to `bind()` inside the block fails, the scope fails, cancelling +all other children. The example below demonstrates a computationally expensive function that takes five milliseconds to compute being eagerly cancelled as soon as a smaller @@ -187,7 +209,7 @@ suspend fun failsIn5ms(): Result { ... } suspend fun failsIn1ms(): Result { ... } runBlocking { - val result: Result = binding { + val result: Result = coroutineBinding { // this creates a new CoroutineScope val x = async { failsIn5ms().bind() } val y = async { failsIn1ms().bind() } x.await() + y.await() @@ -207,20 +229,15 @@ Result monad is present, including: - [Rust](https://doc.rust-lang.org/std/result/) - [Scala](http://www.scala-lang.org/api/2.12.4/scala/util/Either.html) -It also iterates on other Result libraries written in Kotlin, namely: - -- [danneu/kotlin-result](https://github.com/danneu/kotlin-result) -- [kittinunf/Result](https://github.com/kittinunf/Result) -- [npryce/result4k](https://github.com/npryce/result4k) - -Improvements on the existing solutions include: +Improvements on existing solutions such the stdlib include: +- Reduced runtime overhead with zero object allocations on the happy path - 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 +- Top level `Ok` and `Err` functions avoids qualifying usages with `Result.Ok`/`Result.Err` respectively - Higher-order functions marked with the `inline` keyword for reduced runtime overhead @@ -239,52 +256,10 @@ 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. `/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 +[`SQLException`][customer-42] to demonstrate how the `Result` type can +[map internal program errors][update-customer-error] 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": "michael@example.com" -} -``` - -#### Add new customer - -``` -$ curl -i -X POST \ - -H "Content-Type:application/json" \ - -d \ -'{ - "firstName": "Your", - "lastName": "Name", - "email": "email@example.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][github]. @@ -295,22 +270,27 @@ This project is available under the terms of the ISC license. See the [`LICENSE`](LICENSE) file for the copyright information and licensing terms. [result]: https://github.com/michaelbull/kotlin-result/blob/master/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Result.kt#L10 -[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 +[result-value]: https://github.com/michaelbull/kotlin-result/blob/master/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Result.kt#L55 +[result-error]: https://github.com/michaelbull/kotlin-result/blob/master/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Result.kt#L59 +[result-Ok]: https://github.com/michaelbull/kotlin-result/blob/master/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Result.kt#L9 +[result-Err]: https://github.com/michaelbull/kotlin-result/blob/master/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Result.kt#L17 +[kotlin-inline-classes]: https://kotlinlang.org/docs/inline-classes.html +[wiki-Overhead]: https://github.com/michaelbull/kotlin-result/wiki/Overhead +[rop]: https://fsharpforfunandprofit.com/rop/ +[kotlin-native-target-support]: https://kotlinlang.org/docs/native-target-support.html +[github]: https://github.com/michaelbull/kotlin-result [wiki]: https://github.com/michaelbull/kotlin-result/wiki +[result-runCatching]: https://github.com/michaelbull/kotlin-result/blob/master/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Factory.kt#L11 +[result-binding]: https://github.com/michaelbull/kotlin-result/blob/master/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Binding.kt#L28 +[bow-bindings]: https://bow-swift.io/docs/patterns/monad-comprehensions/#bindings +[result-coroutineBinding]: https://github.com/michaelbull/kotlin-result/blob/master/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/CoroutineBinding.kt#L42 +[kotlin-coroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html [unit-tests]: https://github.com/michaelbull/kotlin-result/tree/master/kotlin-result/src/commonTest/kotlin/com/github/michaelbull/result [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/ [customer-42]: https://github.com/michaelbull/kotlin-result/blob/master/example/src/main/kotlin/com/github/michaelbull/result/example/repository/InMemoryCustomerRepository.kt#L38 [update-customer-error]: https://github.com/michaelbull/kotlin-result/blob/master/example/src/main/kotlin/com/github/michaelbull/result/example/service/CustomerService.kt#L50 -[github]: https://github.com/michaelbull/kotlin-result -[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