Skip to content

Instantly share code, notes, and snippets.

@marcellogalhardo
Created June 7, 2022 16:24
Show Gist options
  • Save marcellogalhardo/29848f0f69b62bff7734c672b1a326d5 to your computer and use it in GitHub Desktop.
Save marcellogalhardo/29848f0f69b62bff7734c672b1a326d5 to your computer and use it in GitHub Desktop.
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