Skip to content

Overhead

Michael Bull edited this page Mar 22, 2024 · 2 revisions

The base Result class is modelled as an inline value class.

Calls to Ok and Err do not create a new instance of the Ok/Err objects - instead these are top-level functions that return a type of Result. This achieves code that produces zero object allocations when on the "happy path", i.e. anything that returns an Ok(value).

The Err(error) function does allocate 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 an example of the bytecode decompiled to Java. Despite three calls to Ok and one call to Err, there are zero object allocations on the happy path, and just one on the unhappy path.

Kotlin:

sealed interface Error
object ErrorOne : Error
object ErrorTwo : Error

fun one(): Result<Int, ErrorOne> {
    return Ok(50)
}

fun two(): Int {
    return 100
}

fun three(int: Int): Result<Int, Error> {
    return Ok(int + 25)
}

fun example() {
    val result = one()
        .map { two() }
        .mapError { ErrorTwo }
        .andThen(::three)
        .toString()

    println(result)
}

Decompiled Java:

public final class Example {
    @NotNull
    public static final Example INSTANCE = new Example();

    private Example() {
    }

    @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 + ')';
        }
    }
}
Clone this wiki locally