Skip to content

Instantly share code, notes, and snippets.

@saantiaguilera
Created January 14, 2021 17:07
Show Gist options
  • Save saantiaguilera/50af0314906a41bca238492cb21d03ba to your computer and use it in GitHub Desktop.
Save saantiaguilera/50af0314906a41bca238492cb21d03ba to your computer and use it in GitHub Desktop.
Result sealed class for kotlin error handling, allowing us to write safe code either in a functional or imperative format, without the use of exceptions. Since kotlin from 1.3 onwards changed their Result into an inline class, we can no longer use it for return values (which eliminates the bad smell of exceptions as errors, like most modern lang…
// Don't forget to add the following options to enable the contracts API
compileKotlin {
kotlinOptions {
freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
}
}
compileTestKotlin {
kotlinOptions {
freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
}
}
package com.saantiaguilera
import com.saantiaguilera.Result.Failure
import com.saantiaguilera.Result.Success
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/**
* A discriminated union that encapsulates successful outcome with a value of type [S]
* or a failure with an arbitrary [Throwable].
*
* This class contains an inlined API for better result/throwable handling, although as they are
* sealed class one can simply switch between them and kotlin will infer the rest.
*
* # Examples
*
* ## Managing results with when expressions
*
* For simply managing a result, one can directly use a `when` with the result as argument. This is because
* the classes are sealed, which makes the `when` _exhaustive_
* ```
* when (Result) {
* is Result.Failure<YourData> -> myMethodOnFailure(Result.error) // error => Throwable
* is Result.Success<YourData> -> myMethodOnSuccess(Result.result) // result => YourData
* }
* ```
*
* ## Using encapsulations
*
* If we don't need to return anything, we can encapsulate our behaviour inside a [onSuccess] and
* [onFailure].
*
* ```
* Result
* .onSuccess { result -> myMethodOnSuccess(result) } // result => YourData
* .onFailure { error -> myMethodOnFailure(error) } // error => Throwable
* ```
*
* ## Simple accessors
*
* You can use simple accesors for a [Result]
*
* ```
* Result.getOrNull() // Get the element, or null if it was an error / the element is null.
* Result.exceptionOrNull() // Get the error, or null if there wasn't an error
* Result.getOrElse { error -> /* do something */ } // Get element if success. Run block and return your result if failure.
* Result.getOrDefault(defaultValue) // Get element if success, defaultValue if failure.
* Result.getOrThrow() // Get the element if success, throw the error if failure. Same as doing Result.throwOnFailure().getOrNull()
* ```
*
* ### Value accessors
*
* They are data classes, so you can simply access them if you know their type.
*
* ```
* if (Result is Result.Success<YourData>) {
* val element: YourData = Result.result
* }
* if (Result is Result.Failure<YourData>) {
* val error: Throwable = Result.error
* }
* ```
*
* ## Managing failures
*
* If you already know how to handle specific types of errors or you want to perform the same action
* always, we already provide out of the box some of them
*
* ### Throw on error
*
* You can directly throw the error on the same loop if you want to broadcast it to the parent
*
* ```
* Result
* .throwOnFailure() // Throws an exception in case of failure. Otherwise, it returns a Result.Success<S>
* .result // Get the result
* ```
*
* ### Silence on error
*
* If you don't care about the error, you can transform it into a [Result.Success] of [null]
* value. Be careful to handle null values!
*
* ```
* Result
* .silenceOnFailure() // Returns a Result.Success<S?>(null)
* .result // Get the result
* ```
*
* ### Recover errors
*
* You can, of course, recover from errors with your own behaviour! If you might throw exceptions, we
* also provide [recoverCatching] which will silence errors you may throw. Of course they will
* come as [Failure] results.
*
* ```
* Result
* .recover { error -> Result.Success<S>(recoverFrom(error)) } // Create a success from the error. If we'd throw here, it will broadcast.
* .result // Get the result
* ```
*
* ```
* Result
* .recoverCatching { error -> throw RuntimeException("error") } // We are throwing here, so it will become a Result.Failure
* // This can still be a Result.Failure!
* ```
*
* ## Transformations
*
* ### Map
*
* If you want to perform transformations on your results, we allow you to map a result into another,
* mutating its value.
*
* You can also use [mapCatching] if you can throw exceptions. Throwing inside the scope will return
* a [Failure] result.
*
* ```
* Result.success("hello")
* .map { result -> "$result world" } // If we throw here, it will broadcast.
* // This can return Success or Failure!
* ```
*
* ```
* Result.success("hello")
* .mapCatching { result -> throw RuntimeException("error") } // If we throw here, its a Result.Failure
* // This can return Success or Failure!
* ```
*
* ### Fold
*
* Fold a [Result] into a specific value. This admits 2 functions, one for each type [Success]/[Failure]
*
* ```
* val string: String = Result
* .fold({ result ->
* "result: $result"
* }, { error ->
* "error: $error"
* }) // We 'transform' the Result into a String.
* ```
*/
sealed class Result<out S> {
class Failure<out S>(val error: Throwable) : Result<S>()
class Success<out S>(val result: S) : Result<S>()
// value & exception retrieval
/**
* Returns the encapsulated value if this instance represents [success][Result.Success] or `null`
* if it is [failure][Result.Failure].
*
* This function is shorthand for `getOrElse { null }` (see [getOrElse]) or
* `fold(onSuccess = { it }, onFailure = { null })` (see [fold]).
*/
inline fun getOrNull(): S? = when (this) {
is Failure<S> -> null
is Success<S> -> result
}
/**
* Returns the encapsulated exception if this instance represents [failure][Result.Failure] or `null`
* if it is [success][Result.Success].
*
* This function is shorthand for `fold(onSuccess = { null }, onFailure = { it })` (see [fold]).
*/
fun exceptionOrNull(): Throwable? = when (this) {
is Failure<S> -> error
is Success<S> -> null
}
/**
* Deconstructs the current result into a nullable success result.
*
* Useful in conjunction with [component2], for destructuring a complete result into all the available cases:
* ```
* // getSomething: () -> Result<Something>
* val (something, error) = getSomething()
* if (error == null) { ... }
* ...
* ```
*/
operator fun component1(): S? {
if (this is Success<S>) {
return result
}
return null
}
/**
* Deconstructs the current result into a nullable failure result.
*
* Useful in conjunction with [component1], for destructuring a complete result into all the available cases:
* ```
* // getSomething: () -> Result<Something>
* val (something, error) = getSomething()
* if (error == null) { ... }
* ...
* ```
*/
operator fun component2(): Throwable? {
if (this is Failure<S>) {
return error
}
return null
}
// companion with constructors
/**
* Companion object for [Result] class that contains its constructor functions
* [success] and [failure].
*
* They are used for hiding the sealed classes implementations.
*/
companion object {
/**
* Returns an instance that encapsulates the given [value] as successful value.
*/
inline fun <S> success(value: S): Result<S> = Success(value)
/**
* Returns an instance that encapsulates the given [throwable] as failure.
*/
inline fun <S> failure(throwable: Throwable): Result<S> = Failure(throwable)
}
}
/**
* Throws exception if the result is failure. This function minimizes
* inlined bytecode for [getOrThrow] and makes sure that in the future we can
* add some exception-augmenting logic here (if needed).
*/
inline fun <S> Result<S>.throwOnFailure(): Result.Success<S> = when (this) {
is Result.Failure<S> -> throw error
is Result.Success<S> -> this
}
/**
* Returns a [Success][Result.Success] with a [null] result if the result is failure.
*/
inline fun <S> Result<S>.silenceOnFailure(): Result.Success<S?> = when (this) {
is Result.Failure<S> -> Result.Success<S?>(null)
is Result.Success<S> -> this
}
/**
* Calls the specified function [block] with `this` value as its receiver and returns its encapsulated result
* if invocation was successful, catching and encapsulating any thrown exception as a failure.
*/
inline fun <T, R> Result<T>.runCatching(block: Result<T>.() -> R): Result<R> {
return try {
Result.success(block())
} catch (e: Exception) {
Result.failure(e)
} catch (e: Exception) {
Result.failure(Error("${e.message}"))
}
}
// -- extensions ---
/**
* Returns the encapsulated value if this instance represents [success][Result.isSuccess] or throws the encapsulated exception
* if it is [failure][Result.isFailure].
*
* This function is shorthand for `getOrElse { throw it }` (see [getOrElse]).
*/
inline fun <T> Result<T>.getOrThrow(): T {
return when (this) {
is Result.Failure<T> -> throw error
is Result.Success<T> -> result
}
}
/**
* Returns the encapsulated value if this instance represents [success][Result.isSuccess] or the
* result of [onFailure] function for encapsulated exception if it is [failure][Result.isFailure].
*
* Note, that an exception thrown by [onFailure] function is rethrown by this function.
*
* This function is shorthand for `fold(onSuccess = { it }, onFailure = onFailure)` (see [fold]).
*/
@OptIn(ExperimentalContracts::class)
inline fun <R, T : R> Result<T>.getOrElse(onFailure: (exception: Throwable) -> R): R {
contract {
callsInPlace(onFailure, InvocationKind.AT_MOST_ONCE)
}
return when (this) {
is Result.Failure<T> -> onFailure(error)
is Result.Success<T> -> result
}
}
/**
* Returns the encapsulated value if this instance represents [success][Result.isSuccess] or the
* [defaultValue] if it is [failure][Result.isFailure].
*
* This function is shorthand for `getOrElse { defaultValue }` (see [getOrElse]).
*/
inline fun <R, T : R> Result<T>.getOrDefault(defaultValue: R): R = when (this) {
is Result.Failure<T> -> defaultValue
is Result.Success<T> -> result
}
/**
* Returns the the result of [onSuccess] for encapsulated value if this instance represents [success][Result.isSuccess]
* or the result of [onFailure] function for encapsulated exception if it is [failure][Result.isFailure].
*
* Note, that an exception thrown by [onSuccess] or by [onFailure] function is rethrown by this function.
*/
@OptIn(ExperimentalContracts::class)
inline fun <R, T> Result<T>.fold(
onSuccess: (value: T) -> R,
onFailure: (exception: Throwable) -> R
): R {
contract {
callsInPlace(onSuccess, InvocationKind.AT_MOST_ONCE)
callsInPlace(onFailure, InvocationKind.AT_MOST_ONCE)
}
return when (this) {
is Result.Success -> onSuccess(result)
is Result.Failure -> onFailure(error)
}
}
// transformation
/**
* Returns the encapsulated result of the given [transform] function applied to encapsulated value
* if this instance represents [success][Result.isSuccess] or the internal exception
* if it is [failure][Result.isFailure].
*
* Note, that an exception thrown by [transform] function is rethrown by this function.
* See [mapCatching] for an alternative that encapsulates exceptions.
*/
@OptIn(ExperimentalContracts::class)
inline fun <R, T> Result<T>.map(transform: (value: T) -> R): Result<R> {
contract {
callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
}
return when (this) {
is Result.Success<T> -> Result.success(transform(result))
is Result.Failure<T> -> Result.failure(error)
}
}
/**
* Returns the encapsulated result of the given [transform] function applied to encapsulated value
* if this instance represents [success][Result.isSuccess] or the transformation feeding
* null if it is [failure][Result.isFailure].
*
* Any exception thrown by [transform] function is caught, encapsulated as a failure and returned by this function.
* See [map] for an alternative that rethrows exceptions.
*/
inline fun <R, T> Result<T>.mapCatching(transform: (value: T) -> R): Result<R> {
return when (this) {
is Result.Success<T> -> runCatching { transform(result) }
is Result.Failure<T> -> Result.failure(error)
}
}
/**
* Returns the encapsulated result of the given [transform] function applied to encapsulated exception
* if this instance represents [failure][Result.isFailure] or the
* original encapsulated value if it is [success][Result.isSuccess].
*
* Note, that an exception thrown by [transform] function is rethrown by this function.
* See [recoverCatching] for an alternative that encapsulates exceptions.
*/
@OptIn(ExperimentalContracts::class)
inline fun <R, T : R> Result<T>.recover(transform: (exception: Throwable) -> R): Result.Success<R> {
contract {
callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
}
return when (this) {
is Result.Failure<T> -> Result.Success(transform(exceptionOrNull()!!))
is Result.Success<T> -> this
}
}
/**
* Returns the encapsulated result of the given [transform] function applied to encapsulated exception
* if this instance represents [failure][Result.isFailure] or the
* original encapsulated value if it is [success][Result.isSuccess].
*
* Any exception thrown by [transform] function is caught, encapsulated as a failure and returned by this function.
* See [recover] for an alternative that rethrows exceptions.
*/
inline fun <R, T : R> Result<T>.recoverCatching(transform: (exception: Throwable) -> R): Result<R> {
return when (val exception = exceptionOrNull()) {
null -> this
else -> runCatching { transform(exception) }
}
}
// "peek" onto value/exception and pipe
/**
* Performs the given [action] on encapsulated exception if this instance represents [failure][Result.isFailure].
* Returns the original `Result` unchanged.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T> Result<T>.onFailure(action: (exception: Throwable) -> Unit): Result<T> {
contract {
callsInPlace(action, InvocationKind.AT_MOST_ONCE)
}
exceptionOrNull()?.let { action(it) }
return this
}
/**
* Performs the given [action] on encapsulated value if this instance represents [success][Result.isSuccess].
* Returns the original `Result` unchanged.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T> Result<T>.onSuccess(action: (value: T) -> Unit): Result<T> {
contract {
callsInPlace(action, InvocationKind.AT_MOST_ONCE)
}
if (this is Result.Success<T>) {
action(result)
}
return this
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment