Add flatMap{Both,Either}

This commit is contained in:
Michael Bull 2024-03-05 17:06:01 +00:00
parent 5d2c732ae7
commit 4e5cdeede7
2 changed files with 371 additions and 236 deletions

View File

@ -22,6 +22,151 @@ public inline infix fun <V, E, U> Result<V, E>.map(transform: (V) -> U): Result<
}
}
/**
* Maps this [Result<Result<V, E>, E>][Result] to [Result<V, E>][Result].
*
* - Rust: [Result.flatten](https://doc.rust-lang.org/std/result/enum.Result.html#method.flatten)
*/
public inline fun <V, E> Result<Result<V, E>, E>.flatten(): Result<V, E> {
return when (this) {
is Ok -> value
is Err -> this
}
}
/**
* Maps this [Result<V, E>][Result] to [Result<U, E>][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 <V, E, U> Result<V, E>.flatMap(transform: (V) -> Result<U, E>): Result<U, E> {
contract {
callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
}
return andThen(transform)
}
/**
* Maps this [Result<V, E>][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 <V, E, U> Result<V, E>.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<V, E>][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 <V, E, U> Result<V, E>.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<V, E>][Result] to [Result<U, E>][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 <V, E, U> Result<V, E>.flatMapBoth(
success: (V) -> Result<U, E>,
failure: (E) -> Result<U, E>,
): Result<U, E> {
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<V, E>][Result] to [Result<U, F>][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 <V, E, U, F> Result<V, E>.mapEither(
success: (V) -> U,
failure: (E) -> F,
): Result<U, F> {
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<V, E>][Result] to [Result<U, F>][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 <V, E, U, F> Result<V, E>.flatMapEither(
success: (V) -> Result<U, F>,
failure: (E) -> Result<U, F>,
): Result<U, F> {
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<V, E>][Result] to [Result<V, F>][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 <V, E, U> Result<Iterable<V>, E>.mapAll(transform: (V) -
}
}
/**
* Maps this [Result<V, E>][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 <V, E, U> Result<V, E>.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<V, E>][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 <V, E, U> Result<V, E>.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<V, E>][Result] to [Result<U, F>][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 <V, E, U, F> Result<V, E>.mapEither(success: (V) -> U, failure: (E) -> F): Result<U, F> {
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<V, E>][Result] to [Result<U, E>][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 <V, E, U> Result<V, E>.flatMap(transform: (V) -> Result<U, E>): Result<U, E> {
contract {
callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
}
return andThen(transform)
}
/**
* Maps this [Result<Result<V, E>, E>][Result] to [Result<V, E>][Result].
*
* - Rust: [Result.flatten](https://doc.rust-lang.org/std/result/enum.Result.html#method.flatten)
*/
public inline fun <V, E> Result<Result<V, E>, E>.flatten(): Result<V, E> {
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 <V, E> Result<V, E>.toErrorIf(predicate: (V) -> Boolean, trans
} else {
this
}
is Err -> this
}
}
@ -215,6 +279,7 @@ public inline fun <V, E> Result<V?, E>.toErrorIfNull(error: () -> E): Result<V,
} else {
Ok(value)
}
is Err -> this
}
}
@ -237,6 +302,7 @@ public inline fun <V, E> Result<V, E>.toErrorUnless(predicate: (V) -> Boolean, t
} else {
this
}
is Err -> this
}
}
@ -258,6 +324,7 @@ public inline fun <V, E> Result<V, E>.toErrorUnlessNull(error: () -> E): Result<
} else {
Err(error())
}
is Err -> Err(error())
}
}

View File

@ -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<Int, MapErr> = Ok(10)
assertEquals(
expected = 70,
actual = value
expected = Ok(30),
actual = value.map { it + 20 }
)
}
@Test
fun returnsErrorIfErr() {
val result: Result<String, MapErr> = 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<Int, MapErr> = 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<Int, MapErr> = Ok(70)
assertEquals(
expected = Ok(70),
actual = value.mapError { MapErr.WorldError }
)
}
@Test
fun returnsErrorIfErr() {
val value: Result<Int, MapErr> = Err(MapErr.HelloError)
assertEquals(
expected = Err(MapErr.WorldError),
actual = value.mapError { MapErr.WorldError }
)
}
}
class MapOr {
@Test
fun returnsTransformedValueIfOk() {
val value: Result<String, String> = Ok("foo")
assertEquals(
expected = 3,
actual = value.mapOr(42, String::length)
)
}
@Test
fun returnsDefaultValueIfErr() {
val value: Result<String, String> = Err("foo")
assertEquals(
expected = 42,
actual = value.mapOr(42, String::length)
)
}
}
class MapOrElse {
private val k = 21
@Test
fun returnsTransformedValueIfOk() {
val value: Result<String, String> = Ok("foo")
assertEquals(
expected = 3,
actual = value.mapOrElse({ k * 2 }, String::length)
)
}
@Test
fun returnsDefaultValueIfErr() {
val value: Result<String, String> = Err("foo")
assertEquals(
expected = 42,
actual = value.mapOrElse({ k * 2 }, String::length)
)
}
}
class MapBoth {
@Test
fun returnsTransformedValueIfOk() {
val value: Result<Int, Long> = 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<Int, Long> = 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<Int, Long> = Ok(50)
val result: Result<String, Long> = value.flatMapBoth(
success = { Ok("good $it") },
failure = { Err(100L) }
)
assertEquals(
expected = Ok("good 50"),
actual = result
)
}
@Test
fun returnsTransformedErrorIfErr() {
val result: Result<Int, Long> = Err(25L)
val value: Result<String, Long> = result.flatMapBoth(
success = { Ok("good $it") },
failure = { Err(100L) }
)
assertEquals(
expected = Err(100L),
actual = value
)
}
}
class MapEither {
@Test
fun returnsTransformedValueIfOk() {
val value: Result<Int, MapErr.HelloError> = Ok(500)
val result: Result<Long, MapErr.CustomError> = value.mapEither(
success = { it + 500L },
failure = { MapErr.CustomError("$it") }
)
assertEquals(
expected = Ok(1000L),
actual = result
)
}
@Test
fun returnsTransformedErrorIfErr() {
val value: Result<Int, MapErr.HelloError> = Err(MapErr.HelloError)
val result: Result<Long, MapErr.CustomError> = 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<Int, MapErr.HelloError> = Ok(500)
val result: Result<Long, MapErr.CustomError> = value.flatMapEither(
success = { Ok(it + 500L) },
failure = { Err(MapErr.CustomError("$it")) }
)
assertEquals(
expected = Ok(1000L),
actual = result
)
}
@Test
fun returnsTransformedErrorIfErr() {
val value: Result<Int, MapErr.HelloError> = Err(MapErr.HelloError)
val result: Result<Long, MapErr.CustomError> = value.flatMapEither(
success = { Ok(it + 500L) },
failure = { Err(MapErr.CustomError("bad")) }
)
assertEquals(
expected = Err(MapErr.CustomError("bad")),
actual = result
)
}
}
class ToErrorIfNull {
@Test