- The Result type is now an inline value class for reduced runtime overhead (981fbe2)
- Before & After comparisons outlined below
- Also see the Overhead design doc on the wiki
- Previously deprecated behaviours have been removed (eecd1b7)
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:
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)
}
}
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.
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
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
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 + ')';
}
}
}