Created
June 7, 2022 16:24
-
-
Save marcellogalhardo/29848f0f69b62bff7734c672b1a326d5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import kotlinx.coroutines.CancellationException | |
import kotlin.contracts.ExperimentalContracts | |
import kotlin.contracts.InvocationKind | |
import kotlin.contracts.contract | |
/** | |
* A discriminated union that encapsulates a successful outcome with a value of type [T] | |
* or a failure with an arbitrary [Throwable] exception. | |
* | |
* While using Kotlin's [Result] with [runCatching] method is very simple, there are some flaws: | |
* | |
* - [runCatching] catches [Throwable]. This also means it catches [Error]. | |
* See [difference between using Throwable and Exception in a try catch](https://stackoverflow.com/questions/2274102/difference-between-using-throwable-and-exception-in-a-try-catch/2274116#2274116) | |
* for why this should not be always be done. | |
* - `runCatching` does not rethrow [CancellationException]. This makes it a bad choice for | |
* coroutines as it breaks structured concurrency. | |
* | |
* See https://github.com/Kotlin/kotlinx.coroutines/issues/1814. | |
*/ | |
public sealed class Outcome<out T> { | |
@PublishedApi | |
internal data class Failure<out T>(val exception: Exception) : Outcome<T>() | |
@PublishedApi | |
internal data class Success<out T>(val value: T) : Outcome<T>() | |
/** | |
* Returns `true` if this instance represents a failed outcome. | |
* In this case [isSuccess] returns `false`. | |
*/ | |
public val isFailure: Boolean | |
get() = this is Failure | |
/** | |
* Returns `true` if this instance represents a successful outcome. | |
* In this case [isFailure] returns `false`. | |
*/ | |
public val isSuccess: Boolean | |
get() = this is Success | |
/** | |
* Returns the encapsulated value if this instance represents [success][Outcome.isSuccess] or `null` | |
* if it is [failure][Outcome.isFailure]. | |
* | |
* This function is a shorthand for `getOrElse { null }` (see [getOrElse]) or | |
* `fold(onSuccess = { it }, onFailure = { null })` (see [fold]). | |
*/ | |
public fun getOrNull(): T? = when (this) { | |
is Failure -> null | |
is Success -> value | |
} | |
/** | |
* Returns the encapsulated [Throwable] exception if this instance represents [failure][isFailure] or `null` | |
* if it is [success][isSuccess]. | |
* | |
* This function is a shorthand for `fold(onSuccess = { null }, onFailure = { it })` (see [fold]). | |
*/ | |
public fun exceptionOrNull(): Throwable? = when (this) { | |
is Failure -> exception | |
is Success -> null | |
} | |
/** | |
* Returns a string `Success(v)` if this instance represents [success][Outcome.isSuccess] | |
* where `v` is a string representation of the value or a string `Failure(x)` if | |
* it is [failure][isFailure] where `x` is a string representation of the exception. | |
*/ | |
public override fun toString(): String = when (this) { | |
is Failure -> "Failure($exception)" | |
is Success -> "Success($value)" | |
} | |
/** | |
* Companion object for [Outcome] class that contains its constructor functions | |
* [success] and [failure]. | |
*/ | |
public companion object { | |
/** | |
* Returns an instance that encapsulates the given [value] as successful value. | |
*/ | |
public fun <T> success(value: T): Outcome<T> = Success(value) | |
/** | |
* Returns an instance that encapsulates the given [Exception] as failure. | |
*/ | |
public fun <T> failure(exception: Exception): Outcome<T> = Failure(exception) | |
} | |
} | |
/** | |
* Calls the specified function [block] and returns its encapsulated result if invocation was successful, | |
* catching any [Throwable] exception that was thrown from the [block] function execution and encapsulating it as a failure. | |
*/ | |
@Suppress("FunctionName", "TooGenericExceptionCaught") | |
public inline fun <T> Outcome(block: () -> T): Outcome<T> { | |
return try { | |
Outcome.success(block()) | |
} catch (e: CancellationException) { | |
throw e | |
} catch (e: Exception) { | |
Outcome.failure(e) | |
} | |
} | |
/** | |
* Returns the encapsulated value if this instance represents [success][Outcome.isSuccess] or throws the encapsulated [Throwable] exception | |
* if it is [failure][Outcome.isFailure]. | |
* | |
* This function is a shorthand for `getOrElse { throw it }` (see [getOrElse]). | |
*/ | |
public fun <T> Outcome<T>.getOrThrow(): T { | |
return when (this) { | |
is Outcome.Failure -> throw exception | |
is Outcome.Success -> value | |
} | |
} | |
/** | |
* Returns the encapsulated value if this instance represents [success][Outcome.isSuccess] or the | |
* result of [onFailure] function for the encapsulated [Throwable] exception if it is [failure][Outcome.isFailure]. | |
* | |
* Note, that this function rethrows any [Throwable] exception thrown by [onFailure] function. | |
* | |
* This function is a shorthand for `fold(onSuccess = { it }, onFailure = onFailure)` (see [fold]). | |
*/ | |
@OptIn(ExperimentalContracts::class) | |
public inline fun <R, T : R> Outcome<T>.getOrElse(onFailure: (exception: Throwable) -> R): R { | |
contract { | |
callsInPlace(onFailure, InvocationKind.AT_MOST_ONCE) | |
} | |
return when (this) { | |
is Outcome.Failure -> onFailure(exception) | |
is Outcome.Success -> value | |
} | |
} | |
/** | |
* Returns the encapsulated value if this instance represents [success][Outcome.isSuccess] or the | |
* [defaultValue] if it is [failure][Outcome.isFailure]. | |
* | |
* This function is a shorthand for `getOrElse { defaultValue }` (see [getOrElse]). | |
*/ | |
public fun <R, T : R> Outcome<T>.getOrDefault(defaultValue: R): R { | |
return when (this) { | |
is Outcome.Failure -> defaultValue | |
is Outcome.Success -> value | |
} | |
} | |
/** | |
* Returns the result of [onSuccess] for the encapsulated value if this instance represents [success][Outcome.isSuccess] | |
* or the result of [onFailure] function for the encapsulated [Throwable] exception if it is [failure][Outcome.isFailure]. | |
* | |
* Note, that this function rethrows any [Throwable] exception thrown by [onSuccess] or by [onFailure] function. | |
*/ | |
@OptIn(ExperimentalContracts::class) | |
public inline fun <R, T> Outcome<T>.fold( | |
onFailure: (exception: Throwable) -> R, | |
onSuccess: (value: T) -> R, | |
): R { | |
contract { | |
callsInPlace(onFailure, InvocationKind.AT_MOST_ONCE) | |
callsInPlace(onSuccess, InvocationKind.AT_MOST_ONCE) | |
} | |
return when (this) { | |
is Outcome.Failure -> onFailure(exception) | |
is Outcome.Success -> onSuccess(value) | |
} | |
} | |
/** | |
* Returns the encapsulated result of the given [transform] function applied to the encapsulated value | |
* if this instance represents [success][Outcome.isSuccess] or the | |
* original encapsulated [Throwable] exception if it is [failure][Outcome.isFailure]. | |
* | |
* Note, that this function rethrows any [Throwable] exception thrown by [transform] function. | |
* See [mapCatching] for an alternative that encapsulates exceptions. | |
*/ | |
@OptIn(ExperimentalContracts::class) | |
public inline fun <R, T> Outcome<T>.map(transform: (value: T) -> R): Outcome<R> { | |
contract { | |
callsInPlace(transform, InvocationKind.AT_MOST_ONCE) | |
} | |
return when (this) { | |
is Outcome.Failure -> Outcome.failure(exception) | |
is Outcome.Success -> Outcome.success(transform(value)) | |
} | |
} | |
/** | |
* Returns the encapsulated result of the given [transform] function applied to the encapsulated value | |
* if this instance represents [success][Outcome.isSuccess] or the | |
* original encapsulated [Throwable] exception if it is [failure][Outcome.isFailure]. | |
* | |
* This function catches any [Throwable] exception thrown by [transform] function and encapsulates it as a failure. | |
* See [map] for an alternative that rethrows exceptions from `transform` function. | |
*/ | |
public inline fun <R, T> Outcome<T>.mapCatching(transform: (value: T) -> R): Outcome<R> { | |
return when (this) { | |
is Outcome.Failure -> Outcome.failure(exception) | |
is Outcome.Success -> Outcome { transform(value) } | |
} | |
} | |
/** | |
* Returns a [Outcome] of the given [transform] function applied to the encapsulated value | |
* if this instance represents [success][Outcome.isSuccess] or the | |
* original encapsulated [Throwable] exception if it is [failure][Outcome.isFailure]. | |
* | |
* Note, that this function rethrows any [Throwable] exception thrown by [transform] function. | |
* See [flatMapCatching] for an alternative that encapsulates exceptions. | |
*/ | |
@OptIn(ExperimentalContracts::class) | |
public inline fun <R, T> Outcome<T>.flatMap(transform: (value: T) -> Outcome<R>): Outcome<R> { | |
contract { | |
callsInPlace(transform, InvocationKind.AT_MOST_ONCE) | |
} | |
return when (this) { | |
is Outcome.Failure -> Outcome.failure(exception) | |
is Outcome.Success -> transform(value) | |
} | |
} | |
/** | |
* Returns a [Outcome] of the given [transform] function applied to the encapsulated value | |
* if this instance represents [success][Outcome.isSuccess] or the | |
* original encapsulated [Throwable] exception if it is [failure][Outcome.isFailure]. | |
* | |
* This function catches any [Throwable] exception thrown by [transform] function and encapsulates it as a failure. | |
* See [map] for an alternative that rethrows exceptions from `transform` function. | |
*/ | |
public inline fun <R, T> Outcome<T>.flatMapCatching(transform: (value: T) -> Outcome<R>): Outcome<R> { | |
return when (this) { | |
is Outcome.Failure -> Outcome.failure(exception) | |
is Outcome.Success -> Outcome { transform(value).getOrThrow() } | |
} | |
} | |
/** | |
* Returns the encapsulated result of the given [transform] function applied to the encapsulated [Throwable] exception | |
* if this instance represents [failure][Outcome.isFailure] or the | |
* original encapsulated value if it is [success][Outcome.isSuccess]. | |
* | |
* Note, that this function rethrows any [Throwable] exception thrown by [transform] function. | |
* See [recoverCatching] for an alternative that encapsulates exceptions. | |
*/ | |
@OptIn(ExperimentalContracts::class) | |
public inline fun <R, T : R> Outcome<T>.recover(transform: (exception: Throwable) -> R): Outcome<R> { | |
contract { | |
callsInPlace(transform, InvocationKind.AT_MOST_ONCE) | |
} | |
return when (this) { | |
is Outcome.Failure -> Outcome.success(transform(exception)) | |
is Outcome.Success -> this | |
} | |
} | |
/** | |
* Returns the encapsulated result of the given [transform] function applied to the encapsulated [Throwable] exception | |
* if this instance represents [failure][Outcome.isFailure] or the | |
* original encapsulated value if it is [success][Outcome.isSuccess]. | |
* | |
* This function catches any [Throwable] exception thrown by [transform] function and encapsulates it as a failure. | |
* See [recover] for an alternative that rethrows exceptions. | |
*/ | |
public inline fun <R, T : R> Outcome<T>.recoverCatching(transform: (exception: Throwable) -> R): Outcome<R> { | |
return when (this) { | |
is Outcome.Failure -> Outcome { transform(exception) } | |
is Outcome.Success -> this | |
} | |
} | |
/** | |
* Performs the given [action] on the encapsulated [Throwable] exception if this instance represents [failure][Outcome.isFailure]. | |
* Returns the original `Result` unchanged. | |
*/ | |
@OptIn(ExperimentalContracts::class) | |
public inline fun <T> Outcome<T>.onFailure(action: (exception: Throwable) -> Unit): Outcome<T> { | |
contract { | |
callsInPlace(action, InvocationKind.AT_MOST_ONCE) | |
} | |
if (this is Outcome.Failure) action(exception) | |
return this | |
} | |
/** | |
* Performs the given [action] on the encapsulated value if this instance represents [success][Outcome.isSuccess]. | |
* Returns the original `Result` unchanged. | |
*/ | |
@OptIn(ExperimentalContracts::class) | |
public inline fun <T> Outcome<T>.onSuccess(action: (value: T) -> Unit): Outcome<T> { | |
contract { | |
callsInPlace(action, InvocationKind.AT_MOST_ONCE) | |
} | |
if (this is Outcome.Success) action(value) | |
return this | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment