KResult

sealed class KResult<out E, out T>(source)

KResult and its inheritors Success and Failure provide an opinionated, functional result type. While Kotlin has its own kotlin.Result already, the intent of KResult is to be more functional, usable and better integrated.

Creating Results

Results can be created by constructing Success or Failure directly, but there is a variety of helpers and third party types to create results from:

import io.kresult.core.KResult
import io.kresult.core.asKResult
import io.kresult.core.asSuccess
import io.kotest.matchers.shouldBe

fun test() {
// by instance
KResult.Success("test")
.isSuccess() shouldBe true

// by extension function
"test".asSuccess()
.isSuccess() shouldBe true

// by catching exceptions
KResult
.catch {
throw RuntimeException("throws")
}
.isSuccess() shouldBe false

// from nullable
KResult
.fromNullable(null) {
RuntimeException("Value can not be null")
}
.isSuccess() shouldBe false

// from Kotlin Result
Result.success("test")
.asKResult()
.isSuccess() shouldBe true
}

From Extensions

There are more result builders with extensions, e.g. from kresult-arrow:

import io.kresult.integration.arrow.toKResult
import arrow.core.Either

fun test() {
Either.Right("test")
.toKResult()
}

Mapping & Transformation

Results can be transformed in several ways. At the core, KResult implements functional mapping with flatMap, map, flatten, filter, etc. Some of these are extension functions that need to be imported.:

import io.kresult.core.KResult
import io.kresult.core.filter
import io.kresult.core.flatMap
import io.kresult.core.flatten
import io.kotest.matchers.shouldBe

fun test() {
// map
KResult.Success(3)
.map { it - 1 }
.getOrNull() shouldBe 2

// flatMap
KResult.Success("some-p4ss!")
.flatMap {
if (it.length 8) {
KResult.Success(it)
} else {
KResult.Failure(RuntimeException("Password is too short"))
}
} shouldBe KResult.Success("some-p4ss!")

// filter
KResult.Success("some-p4ss!")
.filter(RuntimeException("String is empty")) {
it.isNotBlank()
} shouldBe KResult.Success("some-p4ss!")

// flatten
KResult.Success(KResult.Success(2))
.flatten() shouldBe KResult.Success(2)
}

Getting values or errors

As a KResult is always either a Success or a Failure / FailureWithValue, a simple, kotlin-native when expression or a destructuring declaration can be used to get the Success.value or Failure.error:

Heads up: If you destructure a FailureWithValue, you'll get an error AND a value back!

import io.kresult.core.KResult
import io.kresult.core.KResult.Failure
import io.kresult.core.KResult.FailureWithValue
import io.kresult.core.KResult.Success
import io.kotest.matchers.shouldBe

fun test() {
val res: KResult<String, Int> = Success(2)

// when expression
when (res) {
is Success -> "success: ${res.value}"
is Failure -> "failure: ${res.error}"
is FailureWithValue -> "failure: ${res.error} (original value was ${res.value})"
} shouldBe "success: 2"

// destructuring call
val (error, value) = res

error shouldBe null
value shouldBe 2
}

Convenience Methods

Additionally, convenience methods like fold, getOrNull, getOrDefault, or getOrThrow for results that have a Throwable on failure side.

import io.kresult.core.KResult
import io.kresult.core.getOrDefault
import io.kresult.core.getOrThrow
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe

fun test() {
// fold
KResult.Success(2).fold(
{ "failure: $it" },
{ "success: $it" }
) shouldBe "success: 2"

// getOrNull
KResult.Success(2).getOrNull() shouldBe 2
KResult.Failure("error").getOrNull() shouldBe null

// getOrElse
KResult.Success(2).getOrDefault { -1 } shouldBe 2
KResult.Failure("error").getOrDefault { -1 } shouldBe -1

// getOrThrow
KResult.Success(2).getOrThrow() shouldBe 2
shouldThrow<RuntimeException> {
KResult.Failure(RuntimeException("test")).getOrThrow()
}
}

Multiple Failures or Success values

Returning a list of successful values with a result, as well as indicating multiple failure reasons are common cases. If we are, for example validating user input, we want to indicate all validations that failed, not just the first.

Because of the flexibility of KResult, this can easily be achieved by just representing the Success or Failure side with a List respectively. Examples could be KResult<List<Error>, String> or KResult<List<Error>, List<String>> if we have multiple success results as well. As these type definition can become rather complex, predefined typealiases can be used:

To make working with success or failure lists easier, there are a few conveninet helpers. Keep in mind that the KResult.validate extension can only be used with MultiErrorKResult or more generally, if you have a List on the Failure side:

import io.kresult.core.KResult
import io.kresult.core.MultiErrorKResult
import io.kresult.core.errors
import io.kresult.core.validate
import io.kotest.matchers.shouldBe

data class User(val name: String, val age: Int, val active: Boolean)

enum class ValidationError(val message: String) {
InvalidName("Name must not be empty"),
IllegalAge("User must be over 18 years old"),
Inactive("User must active"),
}

fun test() {
val user: MultiErrorKResult<ValidationError, User> =
KResult.Success(User(name = "Max", age = 17, active = false))

val result: MultiErrorKResult<ValidationError, User> = user
.validate(ValidationError.InvalidName) { u ->
u.name.isNotBlank()
}
.validate(ValidationError.IllegalAge) { u ->
u.age >= 18
}
.validate(ValidationError.Inactive) { u ->
u.active
}

result.isFailure() shouldBe true
result.errors().size shouldBe 2
result.errors()[0] shouldBe ValidationError.IllegalAge
result.errors()[1] shouldBe ValidationError.Inactive
}

Inheritors

Types

Link copied to clipboard
object Companion
Link copied to clipboard
class Failure<out E>(val error: E) : KResult<E, Nothing>

Represents a failed KResult

Link copied to clipboard
class FailureWithValue<out E, out T>(val error: E, val value: T) : KResult<E, T>
Link copied to clipboard
class Success<out T>(val value: T) : KResult<Nothing, T>

Represents a successful KResult

Functions

Link copied to clipboard

Transforms a KResult to a Kotlin native kotlin.Result

Link copied to clipboard
fun <E, T> KResult<E, T>.combine(other: KResult<E, T>, combineFailure: (E, E) -> E, combineSuccess: (T, T) -> T): KResult<E, T>
Link copied to clipboard
operator fun <E : Comparable<E>, T : Comparable<T>> KResult<E, T>.compareTo(other: KResult<E, T>): Int
Link copied to clipboard
open operator fun component1(): E?

Failure component for destructuring declaration

Link copied to clipboard
open operator fun component2(): T?

Success component for destructuring declaration

Link copied to clipboard

Returns a list of errors

Link copied to clipboard
infix inline fun <E, T> KResult<E, T>.failureOrDefault(default: (T) -> E): E

Returns the error of the Failure side, or the result of the default function otherwise

Link copied to clipboard

Runs the Failure.error, if the KResult is a failure or null otherwise

Link copied to clipboard
inline fun <E, T> KResult<E, T>.filter(failureValue: E, condition: (success: T) -> Boolean): KResult<E, T>

Syntactic sugar for filter where instead of a failure function, a value can be provided

inline fun <E, T> KResult<E, T>.filter(failureFn: (success: T) -> E, condition: (success: T) -> Boolean): KResult<E, T>

Filters Success results and transforms to Failure of failureFn if condition result is false

Link copied to clipboard
inline fun <E, T, T1> KResult<E, T>.flatMap(f: (success: T) -> KResult<E, T1>): KResult<E, T1>
Link copied to clipboard
fun <E, T, E1> KResult<E, T>.flatMapFailure(f: (failure: E) -> KResult<E1, T>): KResult<E1, T>
Link copied to clipboard
fun <E, T> KResult<E, KResult<E, T>>.flatten(): KResult<E, T>
Link copied to clipboard
Link copied to clipboard
inline fun <R> fold(ifFailure: (failure: E) -> R, ifSuccess: (success: T) -> R): R

Transforms a KResult into a value of R.

Link copied to clipboard
infix inline fun <E, T> KResult<E, T>.getOrDefault(default: (E) -> T): T

Returns the value of the Success side, or the result of the default function otherwise

Link copied to clipboard
infix inline fun <E, T> KResult<E, T>.getOrElse(default: (E) -> T): T
Link copied to clipboard
fun getOrNull(): T?

Runs the Success.value, if the KResult is a success or null otherwise

Link copied to clipboard
fun <E : Throwable, T> KResult<E, T>.getOrThrow(): T

If a KResult has a Throwable on failure side, this either returns the Success.value or throws the Failure.error

Link copied to clipboard

Indicates if a KResult is a Failure

Link copied to clipboard

Indicates if a KResult is a Success

Link copied to clipboard
inline fun <R> map(f: (success: T) -> R): KResult<E, R>

Maps (transforms) the success value T to a new value R

Link copied to clipboard
inline fun <R> mapFailure(f: (E) -> R): KResult<R, T>

Maps (transforms) the failure value E to a new value R

Link copied to clipboard
fun <T> KResult<T, T>.merge(): T
Link copied to clipboard
inline fun onFailure(action: (failure: E) -> Unit): KResult<E, T>

Runs an action (side-effect) when the KResult is a Failure

Link copied to clipboard
inline fun onSuccess(action: (success: T) -> Unit): KResult<E, T>

Runs an action (side-effect) when the KResult is a Success

Link copied to clipboard
fun swap(): KResult<T, E>

Swap the parameters (T and E) of this Result.

Link copied to clipboard
inline fun <E, T> MultiErrorKResult<E, T>.validate(failureValue: E, expectationFn: (success: T) -> Boolean): MultiErrorKResult<E, T>

Validates a Success or a FailureWithValue against a predicate (expectationFn] and applies failureValue if not fulfilled