diff --git a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Map.kt b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Map.kt index e2fd3ef..c0ce588 100644 --- a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Map.kt +++ b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Map.kt @@ -22,6 +22,151 @@ public inline infix fun Result.map(transform: (V) -> U): Result< } } +/** + * Maps this [Result, E>][Result] to [Result][Result]. + * + * - Rust: [Result.flatten](https://doc.rust-lang.org/std/result/enum.Result.html#method.flatten) + */ +public inline fun Result, E>.flatten(): Result { + return when (this) { + is Ok -> value + is Err -> this + } +} + +/** + * Maps this [Result][Result] to [Result][Result] by either applying the [transform] + * function if this [Result] is [Ok], or returning this [Err]. + * + * This is functionally equivalent to [andThen]. + * + * - Scala: [Either.flatMap](http://www.scala-lang.org/api/2.12.0/scala/util/Either.html#flatMap[AA>:A,Y](f:B=>scala.util.Either[AA,Y]):scala.util.Either[AA,Y]) + */ +public inline infix fun Result.flatMap(transform: (V) -> Result): Result { + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + } + + return andThen(transform) +} + +/** + * Maps this [Result][Result] to [U] by applying either the [success] function if this + * [Result] is [Ok], or the [failure] function if this [Result] is an [Err]. + * + * Unlike [mapEither], [success] and [failure] must both return [U]. + * + * - Elm: [Result.Extra.mapBoth](http://package.elm-lang.org/packages/elm-community/result-extra/2.2.0/Result-Extra#mapBoth) + * - Haskell: [Data.Either.either](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Either.html#v:either) + */ +public inline fun Result.mapBoth( + success: (V) -> U, + failure: (E) -> U, +): U { + contract { + callsInPlace(success, InvocationKind.AT_MOST_ONCE) + callsInPlace(failure, InvocationKind.AT_MOST_ONCE) + } + + return when (this) { + is Ok -> success(value) + is Err -> failure(error) + } +} + +/** + * Maps this [Result][Result] to [U] by applying either the [success] function if this + * [Result] is [Ok], or the [failure] function if this [Result] is an [Err]. + * + * Unlike [mapEither], [success] and [failure] must both return [U]. + * + * This is functionally equivalent to [mapBoth]. + * + * - Elm: [Result.Extra.mapBoth](http://package.elm-lang.org/packages/elm-community/result-extra/2.2.0/Result-Extra#mapBoth) + * - Haskell: [Data.Either.either](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Either.html#v:either) + */ +public inline fun Result.fold( + success: (V) -> U, + failure: (E) -> U, +): U { + contract { + callsInPlace(success, InvocationKind.AT_MOST_ONCE) + callsInPlace(failure, InvocationKind.AT_MOST_ONCE) + } + + return mapBoth(success, failure) +} + +/** + * Maps this [Result][Result] to [Result][Result] by applying either the [success] + * function if this [Result] is [Ok], or the [failure] function if this [Result] is an [Err]. + * + * Unlike [mapEither], [success] and [failure] must both return [U]. + * + * - Elm: [Result.Extra.mapBoth](http://package.elm-lang.org/packages/elm-community/result-extra/2.2.0/Result-Extra#mapBoth) + * - Haskell: [Data.Either.either](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Either.html#v:either) + */ +public inline fun Result.flatMapBoth( + success: (V) -> Result, + failure: (E) -> Result, +): Result { + contract { + callsInPlace(success, InvocationKind.AT_MOST_ONCE) + callsInPlace(failure, InvocationKind.AT_MOST_ONCE) + } + + return when (this) { + is Ok -> success(value) + is Err -> failure(error) + } +} + +/** + * Maps this [Result][Result] to [Result][Result] by applying either the [success] + * function if this [Result] is [Ok], or the [failure] function if this [Result] is an [Err]. + * + * Unlike [mapBoth], [success] and [failure] may either return [U] or [F] respectively. + * + * - Haskell: [Data.Bifunctor.Bimap](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Bifunctor.html#v:bimap) + */ +public inline fun Result.mapEither( + success: (V) -> U, + failure: (E) -> F, +): Result { + contract { + callsInPlace(success, InvocationKind.AT_MOST_ONCE) + callsInPlace(failure, InvocationKind.AT_MOST_ONCE) + } + + return when (this) { + is Ok -> Ok(success(value)) + is Err -> Err(failure(error)) + } +} + +/** + * Maps this [Result][Result] to [Result][Result] by applying either the [success] + * function if this [Result] is [Ok], or the [failure] function if this [Result] is an [Err]. + * + * Unlike [mapBoth], [success] and [failure] may either return [U] or [F] respectively. + * + * - Haskell: [Data.Bifunctor.Bimap](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Bifunctor.html#v:bimap) + */ +public inline fun Result.flatMapEither( + success: (V) -> Result, + failure: (E) -> Result, +): Result { + contract { + callsInPlace(success, InvocationKind.AT_MOST_ONCE) + callsInPlace(failure, InvocationKind.AT_MOST_ONCE) + } + + return when (this) { + is Ok -> success(value) + is Err -> failure(error) + } +} + /** * Maps this [Result][Result] to [Result][Result] by either applying the [transform] * function to the [error][Err.error] if this [Result] is [Err], or returning this [Ok]. @@ -94,88 +239,6 @@ public inline infix fun Result, E>.mapAll(transform: (V) - } } -/** - * Maps this [Result][Result] to [U] by applying either the [success] function if this - * [Result] is [Ok], or the [failure] function if this [Result] is an [Err]. Both of these - * functions must return the same type ([U]). - * - * - Elm: [Result.Extra.mapBoth](http://package.elm-lang.org/packages/elm-community/result-extra/2.2.0/Result-Extra#mapBoth) - * - Haskell: [Data.Either.either](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Either.html#v:either) - */ -public inline fun Result.mapBoth(success: (V) -> U, failure: (E) -> U): U { - contract { - callsInPlace(success, InvocationKind.AT_MOST_ONCE) - callsInPlace(failure, InvocationKind.AT_MOST_ONCE) - } - - return when (this) { - is Ok -> success(value) - is Err -> failure(error) - } -} - -/** - * Maps this [Result][Result] to [U] by applying either the [success] function if this - * [Result] is [Ok], or the [failure] function if this [Result] is an [Err]. Both of these - * functions must return the same type ([U]). - * - * This is functionally equivalent to [mapBoth]. - * - * - Elm: [Result.Extra.mapBoth](http://package.elm-lang.org/packages/elm-community/result-extra/2.2.0/Result-Extra#mapBoth) - * - Haskell: [Data.Either.either](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Either.html#v:either) - */ -public inline fun Result.fold(success: (V) -> U, failure: (E) -> U): U { - contract { - callsInPlace(success, InvocationKind.AT_MOST_ONCE) - callsInPlace(failure, InvocationKind.AT_MOST_ONCE) - } - - return mapBoth(success, failure) -} - -/** - * Maps this [Result][Result] to [Result][Result] by applying either the [success] - * function if this [Result] is [Ok], or the [failure] function if this [Result] is an [Err]. - * - * - Haskell: [Data.Bifunctor.Bimap](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Bifunctor.html#v:bimap) - */ -public inline fun Result.mapEither(success: (V) -> U, failure: (E) -> F): Result { - contract { - callsInPlace(success, InvocationKind.AT_MOST_ONCE) - callsInPlace(failure, InvocationKind.AT_MOST_ONCE) - } - - return when (this) { - is Ok -> Ok(success(value)) - is Err -> Err(failure(error)) - } -} - -/** - * Maps this [Result][Result] to [Result][Result] by either applying the [transform] - * function if this [Result] is [Ok], or returning this [Err]. - * - * This is functionally equivalent to [andThen]. - * - * - Scala: [Either.flatMap](http://www.scala-lang.org/api/2.12.0/scala/util/Either.html#flatMap[AA>:A,Y](f:B=>scala.util.Either[AA,Y]):scala.util.Either[AA,Y]) - */ -public inline infix fun Result.flatMap(transform: (V) -> Result): Result { - contract { - callsInPlace(transform, InvocationKind.AT_MOST_ONCE) - } - - return andThen(transform) -} - -/** - * Maps this [Result, E>][Result] to [Result][Result]. - * - * - Rust: [Result.flatten](https://doc.rust-lang.org/std/result/enum.Result.html#method.flatten) - */ -public inline fun Result, E>.flatten(): Result { - return andThen { it } -} - /** * Returns the [transformation][transform] of the [value][Ok.value] if this [Result] is [Ok] * and satisfies the given [predicate], otherwise this [Result]. @@ -194,6 +257,7 @@ public inline fun Result.toErrorIf(predicate: (V) -> Boolean, trans } else { this } + is Err -> this } } @@ -210,11 +274,12 @@ public inline fun Result.toErrorIfNull(error: () -> E): Result if(value == null) { + is Ok -> if (value == null) { Err(error()) } else { Ok(value) } + is Err -> this } } @@ -237,6 +302,7 @@ public inline fun Result.toErrorUnless(predicate: (V) -> Boolean, t } else { this } + is Err -> this } } @@ -258,6 +324,7 @@ public inline fun Result.toErrorUnlessNull(error: () -> E): Result< } else { Err(error()) } + is Err -> Err(error()) } } diff --git a/kotlin-result/src/commonTest/kotlin/com/github/michaelbull/result/MapTest.kt b/kotlin-result/src/commonTest/kotlin/com/github/michaelbull/result/MapTest.kt index 9a3eb0d..abafea7 100644 --- a/kotlin-result/src/commonTest/kotlin/com/github/michaelbull/result/MapTest.kt +++ b/kotlin-result/src/commonTest/kotlin/com/github/michaelbull/result/MapTest.kt @@ -3,175 +3,41 @@ package com.github.michaelbull.result import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull -import kotlin.test.assertSame class MapTest { - private sealed class MapErr(val reason: String) { - object HelloError : MapErr("hello") - object WorldError : MapErr("world") - class CustomError(reason: String) : MapErr(reason) + private sealed interface MapErr { + val reason: String + + data object HelloError : MapErr { + override val reason = "hello" + } + + data object WorldError : MapErr { + override val reason = "world" + } + + data class CustomError(override val reason: String) : MapErr } class Map { + @Test fun returnsTransformedValueIfOk() { - assertEquals( - expected = 30, - actual = Ok(10).map { it + 20 }.get() - ) - } - - @Test - @Suppress("UNREACHABLE_CODE") - fun returnsErrorIfErr() { - val result = Err(MapErr.HelloError).map { "hello $it" } - - result as Err - - assertSame( - expected = MapErr.HelloError, - actual = result.error - ) - } - } - - class MapError { - @Test - fun returnsValueIfOk() { - val value = Ok(55).map { it + 15 }.mapError { MapErr.WorldError }.get() + val value: Result = Ok(10) assertEquals( - expected = 70, - actual = value + expected = Ok(30), + actual = value.map { it + 20 } ) } @Test fun returnsErrorIfErr() { - val result: Result = Ok("let") - .map { "$it me" } - .andThen { - when (it) { - "let me" -> Err(MapErr.CustomError("$it $it")) - else -> Ok("$it get") - } - } - .mapError { MapErr.CustomError("${it.reason} get what i want") } - - result as Err + val value: Result = Err(MapErr.HelloError) assertEquals( - expected = "let me let me get what i want", - actual = result.error.reason - ) - } - } - - class MapOr { - @Test - fun returnsTransformedValueIfOk() { - val value = Ok("foo").mapOr(42, String::length) - - assertEquals( - expected = 3, - actual = value - ) - } - - @Test - fun returnsDefaultValueIfErr() { - val value = Err("bar").mapOr(42, String::length) - - assertEquals( - expected = 42, - actual = value - ) - } - } - - class MapOrElse { - private val k = 21 - - @Test - fun returnsTransformedValueIfOk() { - val value = Ok("foo").mapOrElse({ k * 2 }, String::length) - - assertEquals( - expected = 3, - actual = value - ) - } - - @Test - fun returnsDefaultValueIfErr() { - val value = Err("bar").mapOrElse({ k * 2 }, String::length) - - assertEquals( - expected = 42, - actual = value - ) - } - } - - class MapBoth { - @Test - @Suppress("UNREACHABLE_CODE") - fun returnsTransformedValueIfOk() { - val value = Ok("there is").mapBoth( - success = { "$it a light" }, - failure = { "$it that never" } - ) - - assertEquals( - expected = "there is a light", - actual = value - ) - } - - @Test - @Suppress("UNREACHABLE_CODE") - fun returnsTransformedErrorIfErr() { - val error = Err(MapErr.CustomError("this")).mapBoth( - success = { "$it charming" }, - failure = { "${it.reason} man" } - ) - - assertEquals( - expected = "this man", - actual = error - ) - } - } - - class MapEither { - @Test - @Suppress("UNREACHABLE_CODE") - fun returnsTransformedValueIfOk() { - val result = Ok(500).mapEither( - success = { it + 500 }, - failure = { MapErr.CustomError("$it") } - ) - - result as Ok - - assertEquals( - expected = 1000, - actual = result.value - ) - } - - @Test - fun returnsTransformedErrorIfErr() { - val result = Err("the reckless").mapEither( - success = { "the wild youth" }, - failure = { MapErr.CustomError("the truth") } - ) - - result as Err - - assertEquals( - expected = "the truth", - actual = result.error.reason + expected = Err(MapErr.HelloError), + actual = value.map { "hello $it" } ) } } @@ -224,6 +90,208 @@ class MapTest { } } + class MapError { + + @Test + fun returnsValueIfOk() { + val value: Result = Ok(70) + + assertEquals( + expected = Ok(70), + actual = value.mapError { MapErr.WorldError } + ) + } + + @Test + fun returnsErrorIfErr() { + val value: Result = Err(MapErr.HelloError) + + assertEquals( + expected = Err(MapErr.WorldError), + actual = value.mapError { MapErr.WorldError } + ) + } + } + + class MapOr { + + @Test + fun returnsTransformedValueIfOk() { + val value: Result = Ok("foo") + + assertEquals( + expected = 3, + actual = value.mapOr(42, String::length) + ) + } + + @Test + fun returnsDefaultValueIfErr() { + val value: Result = Err("foo") + + assertEquals( + expected = 42, + actual = value.mapOr(42, String::length) + ) + } + } + + class MapOrElse { + private val k = 21 + + @Test + fun returnsTransformedValueIfOk() { + val value: Result = Ok("foo") + + assertEquals( + expected = 3, + actual = value.mapOrElse({ k * 2 }, String::length) + ) + } + + @Test + fun returnsDefaultValueIfErr() { + val value: Result = Err("foo") + + assertEquals( + expected = 42, + actual = value.mapOrElse({ k * 2 }, String::length) + ) + } + } + + class MapBoth { + + @Test + fun returnsTransformedValueIfOk() { + val value: Result = Ok(50) + + val result: String = value.mapBoth( + success = { "good $it" }, + failure = { "bad $it" } + ) + + assertEquals( + expected = "good 50", + actual = result + ) + } + + @Test + fun returnsTransformedErrorIfErr() { + val value: Result = Err(20) + + val result: String = value.mapBoth( + success = { "good $it" }, + failure = { "bad $it" } + ) + + assertEquals( + expected = "bad 20", + actual = result + ) + } + } + + class FlatMapBoth { + + @Test + fun returnsTransformedValueIfOk() { + val value: Result = Ok(50) + + val result: Result = value.flatMapBoth( + success = { Ok("good $it") }, + failure = { Err(100L) } + ) + + assertEquals( + expected = Ok("good 50"), + actual = result + ) + } + + @Test + fun returnsTransformedErrorIfErr() { + val result: Result = Err(25L) + + val value: Result = result.flatMapBoth( + success = { Ok("good $it") }, + failure = { Err(100L) } + ) + + assertEquals( + expected = Err(100L), + actual = value + ) + } + } + + class MapEither { + + @Test + fun returnsTransformedValueIfOk() { + val value: Result = Ok(500) + + val result: Result = value.mapEither( + success = { it + 500L }, + failure = { MapErr.CustomError("$it") } + ) + + assertEquals( + expected = Ok(1000L), + actual = result + ) + } + + @Test + fun returnsTransformedErrorIfErr() { + val value: Result = Err(MapErr.HelloError) + + val result: Result = value.mapEither( + success = { it + 500L }, + failure = { MapErr.CustomError("bad") } + ) + + assertEquals( + expected = Err(MapErr.CustomError("bad")), + actual = result + ) + } + } + + class FlatMapEither { + + @Test + fun returnsTransformedValueIfOk() { + val value: Result = Ok(500) + + val result: Result = value.flatMapEither( + success = { Ok(it + 500L) }, + failure = { Err(MapErr.CustomError("$it")) } + ) + + assertEquals( + expected = Ok(1000L), + actual = result + ) + } + + @Test + fun returnsTransformedErrorIfErr() { + val value: Result = Err(MapErr.HelloError) + + val result: Result = value.flatMapEither( + success = { Ok(it + 500L) }, + failure = { Err(MapErr.CustomError("bad")) } + ) + + assertEquals( + expected = Err(MapErr.CustomError("bad")), + actual = result + ) + } + } + class ToErrorIfNull { @Test