Releases: michaelbull/kotlin-result
2.0.0
- 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) {
...
1.1.21
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.
Additions
- Add
flatMapEither
,flatMapBoth
(4e5cdee) - Add
mapCatching
(15fc1ff) - Add
Iterable.allOk
,Iterable.allErr
,Iterable.anyOk
,Iterable.anyErr
,Iterable.countOk
,Iterable.countErr
(6e62d9f) - Add
Iterable.filterValues
,Iterable.filterValuesTo
,Iterable.filterErrors
,Iterable.filterErrorsTo
(f091f50) - Add
transpose
(c46a292) - Return
List
of errors for all variants ofzipOrAccumulate
by @YuitoSato (716109a)- The four-arg and five-arg variants were returning
Collection
instead ofList
.
- The four-arg and five-arg variants were returning
Deprecations
- Deprecate
getAll
,getAllErrors
in favour offilterValues
&filterErrors
(aca9ad9) - Deprecate
ResultBinding
in favour ofBindingScope
(dd5c96f)- This matches the Kotlin stdlib naming convention used for sequences and coroutines.
- Deprecate suspending variant of
binding
in favour ofcoroutineBinding
(b19894a)- This matches the internally-called function named
coroutineScope
, and helps consumers distinguish between the blocking variant that is otherwise only differing in package name. - This should also help convey to readers that structured concurrency will occur within the block.
- This matches the internally-called function named
- Deprecate
Ok
/Err
as return types (7ce7c16)- This is in preparation for the v2 release where these don't exist as types.
- Deprecate
getAll
/getAllErrors
in favour ofvaluesOf
/errorsOf
(522c821)
1.1.20
- Pin GitHub actions by commit hash (8893187)
- Add more build targets for coroutines extensions (a522fbd)
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
- This ensures that we are now supporting all three tiers of Kotlin/Native target support.
1.1.19
- Document the order of output lists in Iterable.kt by @peter-cunderlik-kmed (e81f581)
- Add
zipOrAccumulate
by @YuitoSato (27f0a63) - Update Kotlin to 1.9.20 (05a1e91)
- "In Kotlin 1.9.20, we've also removed a number of previously deprecated targets, namely:"
- iosArm32
- watchosX86
- wasm32
- mingwX86
- linuxMips32
- linuxMipsel32
- See: https://kotl.in/native-targets-tiers
- "In Kotlin 1.9.20, we've also removed a number of previously deprecated targets, namely:"
- Add
andThen{Recover,RecoverIf,RecoverUnless}
by @Jhabkin (d4414b1) - Facilitate mapping to arbitrary types in
and
/or
functions (05d50b7)- See #95
- Extract shared build logic to convention plugins (88e31cd)
1.1.18
1.1.17
- Add
recoverCatching
by @berikv (a6eb86d) - Add more multiplatform build targets by @05nelsonm (6f86d20)
- Migrate to IR-based JavaScript compiler (cc3b3ce)
- Disable compatibility with non-hierarchical multiplatform projects (c1c5036)
- See #71 (comment)
- Migrate to Gradle version catalog by @bitPogo (41fff9e)
- Update Gradle to 8.0.2 (8229a29)
- Update dependencies (6e1c4dd)
- Add
toErrorIfNull
&toErrorUnlessNull
(fd2160c)- See #84
1.1.16
- Enable compatibility with non-hierarchical multiplatform projects (f0195b5)
- See: #71 (comment)
1.1.15
1.1.14
1.1.13
- Update Kotlin to 1.5.31 by @pablisco (b8d4109)
- Replace usages of
useExperimentalAnnotation
by @grodin (4e1bb9d) - Update Gradle to 7.2 (98c8eae)
- Add
Result#orElseThrow
(f236e26) - Add
Result#{throwIf,throwUnless}
by @grodin (3b87373) - Add
runSuspendCatching
andT#runSuspendCatching
by @grodin (2667273)- See #64