A multiplatform Result monad for modelling success or failure operations.
ISC License
Bot releases are visible (Hide)
Ok
/Err
as TypesThe migration to an inline value class means that using Ok
/Err
as types is no longer valid.
Consumers that need to introspect the type of Result
should instead use Result.isOk
/Result.isErr
booleans. This naming scheme matches Rust's is_ok
& is_err
functions.
Before:
public inline fun <V, E, U> Result<V, E>.mapOrElse(default: (E) -> U, transform: (V) -> U): U {
return when (this) {
is Ok -> transform(value)
is Err -> default(error)
}
}
After:
public inline fun <V, E, U> Result<V, E>.mapOrElse(default: (E) -> U, transform: (V) -> U): U {
return when {
isOk -> transform(value)
else -> default(error)
}
}
When changing the return type to another result, e.g. the map
function which goes from Result<V, E>
to Result<U, E>
, consumers are encouraged to use the asOk
/asErr
extension functions in conjunction with the isOk
/isErr
guard.
The example below calls asErr
which unsafely casts the Result<V, E
to Result<Nothing, E>
, which is acceptable given the isOk
check, which satisfies the Result<U, E>
return type.
The asOk
/asOk
functions should not be used outside of a manual type guard via isOk
/isErr
- the cast is unsafe.
public inline infix fun <V, E, U> Result<V, E>.map(transform: (V) -> U): Result<U, E> {
return when {
isOk -> Ok(transform(value))
else -> this.asErr() // unsafely typecasts Result<V, E> to Result<Nothing, E>
}
}
The following previously deprecated behaviours have been removed in v2.
binding
& SuspendableResultBinding
, use coroutineBinding
insteadand
without lambda argument, use andThen
insteadResultBinding
, use BindingScope
insteadgetOr
without lambda argument, use getOrElse
insteadgetErrorOr
without lambda argument, use getErrorOrElse
insteadgetAll
, use filterValues
insteadgetAllErrors
, use filterErrors
insteador
without lambda argument, use orElse
insteadResult.of
, use runCatching
insteadexpect
with non-lazy evaluation of message
expectError
with non-lazy evaluation of message
The base Result
class is now modelled as an inline value class. References to Ok<V>
/Err<E>
as types should be replaced with Result<V, Nothing>
and Result<Nothing, E>
respectively.
Calls to Ok
and Err
still function, but they no longer create a new instance of the Ok
/Err
objects - instead these are top-level functions that return a type of Result
. This change achieves code that produces zero object allocations when on the "happy path", i.e. anything that returns an Ok(value)
. Previously, every successful operation wrapped its returned value in a new Ok(value)
object.
The Err(error)
function still allocates a new object each call by internally wrapping the provided error
with a new instance of a Failure
object. This Failure
class is an internal implementation detail and not exposed to consumers. As a call to Err
is usually a terminal state, occurring at the end of a chain, the allocation of a new object is unlikely to cause a lot of GC pressure unless a function that produces an Err
is called in a tight loop.
Below is a comparison of the bytecode decompiled to Java produced before and after this change. The total number of possible object allocations is reduced from 4 to 1, with 0 occurring on the happy path and 1 occurring on the unhappy path.
public final class Before {
@NotNull
public static final Before INSTANCE = new Before();
private Before() {
}
@NotNull
public final Result<Integer, ErrorOne> one() {
return (Result)(new Ok(50));
}
public final int two() {
return 100;
}
@NotNull
public final Result<Integer, ErrorThree> three(int var1) {
return (Result)(new Ok(var1 + 25));
}
public final void example() {
Result $this$map$iv = this.one(); // object allocation (1)
Result var10000;
if ($this$map$iv instanceof Ok) {
Integer var10 = INSTANCE.two();
var10000 = (Result)(new Ok(var10)); // object allocation (2)
} else {
if (!($this$map$iv instanceof Err)) {
throw new NoWhenBranchMatchedException();
}
var10000 = $this$map$iv;
}
Result $this$mapError$iv = var10000;
if ($this$mapError$iv instanceof Ok) {
var10000 = $this$mapError$iv;
} else {
if (!($this$mapError$iv instanceof Err)) {
throw new NoWhenBranchMatchedException();
}
ErrorTwo var11 = ErrorTwo.INSTANCE;
var10000 = (Result)(new Err(var11)); // object allocation (3)
}
Result $this$andThen$iv = var10000;
if ($this$andThen$iv instanceof Ok) {
int p0 = ((Number)((Ok)$this$andThen$iv).getValue()).intValue();
var10000 = this.three(p0); // object allocation (4)
} else {
if (!($this$andThen$iv instanceof Err)) {
throw new NoWhenBranchMatchedException();
}
var10000 = $this$andThen$iv;
}
String result = var10000.toString();
System.out.println(result);
}
public static abstract class Result<V, E> {
private Result() {
}
}
public static final class Ok<V> extends Result {
private final V value;
public Ok(V value) {
this.value = value;
}
public final V getValue() {
return this.value;
}
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
} else if (other != null && this.getClass() == other.getClass()) {
Ok var10000 = (Ok)other;
return Intrinsics.areEqual(this.value, ((Ok)other).value);
} else {
return false;
}
}
public int hashCode() {
Object var10000 = this.value;
return var10000 != null ? var10000.hashCode() : 0;
}
@NotNull
public String toString() {
return "Ok(" + this.value + ')';
}
}
public static final class Err<E> extends Result {
private final E error;
public Err(E error) {
this.error = error;
}
public final E getError() {
return this.error;
}
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
} else if (other != null && this.getClass() == other.getClass()) {
Before$Err var10000 = (Err)other;
return Intrinsics.areEqual(this.error, ((Err)other).error);
} else {
return false;
}
}
public int hashCode() {
Object var10000 = this.error;
return var10000 != null ? var10000.hashCode() : 0;
}
@NotNull
public String toString() {
return "Err(" + this.error + ')';
}
}
}
public final class After {
@NotNull
public static final After INSTANCE = new After();
private After() {
}
@NotNull
public final Object one() {
return this.Ok(50);
}
public final int two() {
return 100;
}
@NotNull
public final Object three(int var1) {
return this.Ok(var1 + 25);
}
public final void example() {
Object $this$map_u2dj2AeeQ8$iv = this.one();
Object var10000;
if (Result.isOk_impl($this$map_u2dj2AeeQ8$iv)) {
var10000 = this.Ok(INSTANCE.two());
} else {
var10000 = $this$map_u2dj2AeeQ8$iv;
}
Object $this$mapError_u2dj2AeeQ8$iv = var10000;
if (Result.isErr_impl($this$mapError_u2dj2AeeQ8$iv)) {
var10000 = this.Err(ErrorTwo.INSTANCE); // object allocation (1)
} else {
var10000 = $this$mapError_u2dj2AeeQ8$iv;
}
Object $this$andThen_u2dj2AeeQ8$iv = var10000;
if (Result.isOk_impl($this$andThen_u2dj2AeeQ8$iv)) {
int p0 = ((Number) Result.getValue_impl($this$andThen_u2dj2AeeQ8$iv)).intValue();
var10000 = this.three(p0);
} else {
var10000 = $this$andThen_u2dj2AeeQ8$iv;
}
String result = Result.toString_impl(var10000);
System.out.println(result);
}
@NotNull
public final <V> Object Ok(V value) {
return Result.constructor_impl(value);
}
@NotNull
public final <E> Object Err(E error) {
return Result.constructor_impl(new Failure(error));
}
public static final class Result<V, E> {
@Nullable
private final Object inlineValue;
public static final V getValue_impl(Object arg0) {
return arg0;
}
public static final E getError_impl(Object arg0) {
Intrinsics.checkNotNull(arg0, "null cannot be cast to non-null type Failure<E of Result>");
return ((Failure) arg0).getError();
}
public static final boolean isOk_impl(Object arg0) {
return !(arg0 instanceof Failure);
}
public static final boolean isErr_impl(Object arg0) {
return arg0 instanceof Failure;
}
@NotNull
public static String toString_impl(Object arg0) {
return isOk_impl(arg0) ? "Ok(" + getValue_impl(arg0) + ')' : "Err(" + getError_impl(arg0) + ')';
}
@NotNull
public String toString() {
return toString_impl(this.inlineValue);
}
public static int hashCode_impl(Object arg0) {
return arg0 == null ? 0 : arg0.hashCode();
}
public int hashCode() {
return hashCode_impl(this.inlineValue);
}
public static boolean equals_impl(Object arg0, Object other) {
if (!(other instanceof Result)) {
return false;
} else {
return Intrinsics.areEqual(arg0, ((Result) other).unbox_impl());
}
}
public boolean equals(Object other) {
return equals_impl(this.inlineValue, other);
}
private Result(Object inlineValue) {
this.inlineValue = inlineValue;
}
@NotNull
public static <V, E> Object constructor_impl(@Nullable Object inlineValue) {
return inlineValue;
}
public static final Result box_impl(Object v) {
return new Result(v);
}
public final Object unbox_impl() {
return this.inlineValue;
}
public static final boolean equals_impl0(Object p1, Object p2) {
return Intrinsics.areEqual(p1, p2);
}
}
static final class Failure<E> {
private final E error;
public Failure(E error) {
this.error = error;
}
public final E getError() {
return this.error;
}
public boolean equals(@Nullable Object other) {
return other instanceof Failure && Intrinsics.areEqual(this.error, ((Failure)other).error);
}
public int hashCode() {
Object var10000 = this.error;
return var10000 != null ? var10000.hashCode() : 0;
}
@NotNull
public String toString() {
return "Failure(" + this.error + ')';
}
}
}
This release serves as a bridge towards v2 and the last major release of v1.
Old behaviours have been deprecated in a non-breaking manner to anticipate the breaking changes of v2.
flatMapEither
, flatMapBoth
(4e5cdeede7b5dbecc96ea8f561bbec4ae96e407b)mapCatching
(15fc1ff0139306b640a38f00922748b34e2e1d5b)
Iterable.allOk
, Iterable.allErr
, Iterable.anyOk
, Iterable.anyErr
, Iterable.countOk
, Iterable.countErr
(6e62d9f97df43c7d2e10fe407ddc110aeb85840d)Iterable.filterValues
, Iterable.filterValuesTo
, Iterable.filterErrors
, Iterable.filterErrorsTo
(f091f507d97a0d3f565945ef9c97ae1eb28bfe59)transpose
(c46a2925b17a87b98081b7b10214e1468ffc483d)
List
of errors for all variants of zipOrAccumulate
by @YuitoSato (716109aa84ba3d5975b964351b6b0c5bec251ee5)
Collection
instead of List
.getAll
, getAllErrors
in favour of filterValues
& filterErrors
(aca9ad92f8ec761d2fdad5b6ccbf900931eddf74)ResultBinding
in favour of BindingScope
(dd5c96f983b08fc5c44c44e888636f19c0e45b22)
binding
in favour of coroutineBinding
(b19894a08c1ec56512ff301444669da2125f8de2)
coroutineScope
, and helps consumers distinguish between the blocking variant that is otherwise only differing in package name.Ok
/Err
as return types (7ce7c16d7fb055e31711218ac4642a621c8cc515)
getAll
/getAllErrors
in favour of valuesOf
/errorsOf
(522c821fdf45363c0f3d60b29b37bb958af90839)kotlinx-coroutines
has since started publishing more native build targets since we first became multiplatform. This release ensures we also build native targets for the platforms that were previously missing, namely:
androidNativeArm32
androidNativeArm64
androidNativeX64
androidNativeX86
linuxArm64
wasmJs
zipOrAccumulate
by @YuitoSato (27f0a63847a0522686a67bc3672b4c4b73f4c449)andThen{Recover,RecoverIf,RecoverUnless}
by @Jhabkin (d4414b1a086cb219795d649fddf79bbd4c9a4c63)and
/or
functions (05d50b7fec517de0ed7ab76cf7f2085f53fedbf9)
recoverCatching
by @berikv (a6eb86da71e0db1386f2b013eba31f12c53cc6b5)toErrorIfNull
& toErrorUnlessNull
(fd2160c7a66f33c0c29011e59af1a1a514f3ba2a)
Published by michaelbull over 2 years ago
getOrThrow
by @Nimelrian (d07bd589edd24e55d18ec03036ac554e18fae025)
kotlinx-coroutines-test
API (72df4c0ff60979a3782ea8806ad853ee39c962b0)
Published by michaelbull almost 3 years ago
useExperimentalAnnotation
by @grodin (4e1bb9d8de06ad30c3f3ca84bc6c00810587a72d)Result#orElseThrow
(f236e2674bf98f12f50fa740e1f70aa65b5464d5)Result#{throwIf,throwUnless}
by @grodin (3b87373b238df613605cee0a82ee1e0def879507)runSuspendCatching
and T#runSuspendCatching
by @grodin (2667273015dd18350d23f7a8de965cb49c184a8d)
recoverUnless
kdoc (754aa5aaa4da22f875dac733c1fb51bbf21f2b92)Result.recoverIf
and Result.recoverUnless
(0fdd0f2c2bccf3fdc6eb0615a145da4b6ee12ed5) by @oddsundbinding
support moved to kotlin-result-coroutines
module (b16fb559a14dcd77139fb907db213184581e6bd6) by @Munzey
merge
(09d341ae6dc5222a15430e10adae5bcebd64f436)
.merge()
on a Result<List<Int>, Set<Int>>
will return a Collection<Int>
(their most common supertype).kotlin.code.style=official
to gradle.properties (6651b18905feebdbc445741168ee7128e18c5b5e)mapOr
& mapOrElse
(43ebd5753a2fb3810408c573fa09505e16930f4b)
val (value: String?, error: Throwable?) = runCatching(yourFunction)
Published by michaelbull about 5 years ago
Result.of
in favour of runCatching
factory function (586b260683007d3a949d746c616ebc1abcdac449)