Name | Modified | Size | Downloads / Week |
---|---|---|---|
Parent folder | |||
2.0.0 source code.tar.gz | 2024-03-16 | 115.0 kB | |
2.0.0 source code.zip | 2024-03-16 | 167.1 kB | |
README.md | 2024-03-16 | 14.2 kB | |
Totals: 3 Items | 296.4 kB | 0 |
- The Result type is now an inline value class for reduced runtime overhead (981fbe2812185f5083f44854409601bb42fcfb3c)
- Before & After comparisons outlined below
- Also see the Overhead design doc on the wiki
- Previously deprecated behaviours have been removed (eecd1b7d9946f541af3f5d453905024ff97e7c26)
Migration Guide
Ok
/Err
as Types
The 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:
:::kotlin
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:
:::kotlin
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)
}
}
Type Casting
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.
:::kotlin
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>
}
}
Removal of Deprecations
The following previously deprecated behaviours have been removed in v2.
binding
&SuspendableResultBinding
, usecoroutineBinding
insteadand
without lambda argument, useandThen
insteadResultBinding
, useBindingScope
insteadgetOr
without lambda argument, usegetOrElse
insteadgetErrorOr
without lambda argument, usegetErrorOrElse
insteadgetAll
, usefilterValues
insteadgetAllErrors
, usefilterErrors
insteador
without lambda argument, useorElse
insteadResult.of
, userunCatching
insteadexpect
with non-lazy evaluation ofmessage
expectError
with non-lazy evaluation ofmessage
Inline Value Class - Before & After
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.
Before: 4 object allocations, 3 on happy path & 1 on unhappy path
:::java 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 + ')'; } } }
After: 1 object allocation, 0 on happy path & 1 on unhappy path
:::java 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 + ')'; } } }