Search code examples
kotlinlambdainlineprimitive

Are Kotlin scope function blocks effectively inline?


I'm writing a Kotlin inline class to make Decimal4J more convenient without instantiating any objects. I'm worried that scope functions might create lambda objects, thereby making the whole thing pointless.

Consider the function compareTo in the following example.

/* imports and whatnot */

@JvmInline
value class Quantity(val basis: Long) {

    companion object {
        val scale: Int = 12
        val metrics: ScaleMetrics = Scales.getScaleMetrics(scale)
        val arithmetic: DecimalArithmetic = metrics.defaultArithmetic
    }

    operator fun compareTo(alt: Number): Int {
        with(arithmetic) {
            val normal = when (alt) {
                is Double     -> fromDouble(alt)
                is Float      -> fromFloat(alt)
                is Long       -> fromLong(alt)
                is BigDecimal -> fromBigDecimal(alt)
                is BigInteger -> fromBigInteger(alt)
                else          -> fromLong(alt.toLong())
            }
            return compare(basis, normal)
        }
    }
}

Does the with(arithmetic) scope create a lambda in the heap? The docs on kotlinlang.org consistently refer to the scoped code as a lambda expression. Is there any way to use scope functions without creating objects?


Solution

  • All of the built-in scoping functions, including with, are marked inline, which means the implementation gets planted directly in the code that's calling it. Once that happens, the lambda call can be optimized away.

    To be more concrete, here's the implementation of with (with the Kotlin contracts stuff removed, since that's not relevant here)

    public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
      return receiver.block()
    }
    

    Extension methods are, and always have been, syntax sugar resolved at compile time, so this is effectively

    public inline fun <T, R> with(receiver: T, block: (T) -> R): R {
      return block(receiver) // (with `this` renamed by the compiler)
    }
    

    So when we call

    operator fun compareTo(alt: Number): Int {
      with (arithmetic) {
        println("Hi :)")
        println(foobar()) // Assuming foobar is a method on arithmetic
      }
    }
    

    The inline will transform this into

    operator fun compareTo(alt: Number): Int {
      ({
        println("Hi :)")
        println(it.foobar()) // Assuming foobar is a method on arithmetic
      })(arithmetic)
    }
    

    And any optimizer worth its salt can see that this is a function that's immediately evaluated, so we should go ahead and do that now. What we end up with is

    operator fun compareTo(alt: Number): Int {
      println("Hi :)")
      println(arithmetic.foobar()) // Assuming foobar is a method on arithmetic
    }
    

    which is what you would have written to begin with.

    So, tl;dr, the compiler is smart enough to figure it out. You don't have to worry about it. It's one of the perks of working in a high-level language.

    By the way, this isn't just abstract. I just compiled the above code on my own machine and then decompiled the JVM bytecode to see what it really did. It was quite a bit noisier (since the JVM, by necessity, has a lot of noise), but there was no lambda object allocated, and the function was just one straight shot that calls println twice.

    In case you're interested, Kotlin takes this example function

    fun compareTo(alt: Number): Unit {
      return with(arithmetic) {
        println("Hi :)")
        println(foobar())
      }
    }
    

    to this Java, after being decompiled,

    public static final void compareTo-impl(long arg0, @NotNull Number alt) {
        Intrinsics.checkNotNullParameter((Object)alt, (String)"alt");
        long l = arithmetic;
        boolean bl = false;
        boolean bl2 = false;
        long $this$compareTo_impl_u24lambda_u2d0 = l;
        boolean bl3 = false;
        String string = "Hi :)";
        boolean bl4 = false;
        System.out.println((Object)string);
        int n = so_quant.foobar-impl($this$compareTo_impl_u24lambda_u2d0);
        bl4 = false;
        System.out.println(n);
    }
    

    Quite a bit noisier, but the idea is exactly the same. And all of those pointless local variables will be taken care of by a good JIT engine.