Home > Software design >  Are Kotlin scope function blocks effectively inline?
Are Kotlin scope function blocks effectively inline?

Time:08-06

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?

CodePudding user response:

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.

CodePudding user response:

Just some additional info to help clear up the terminology that led to your confusion.

The word “lambda” is defined as a syntax for writing a function. The word does not describe a function itself, so the word lambda has nothing to do with whether a function object is being allocated or not.

In Kotlin, there are multiple different syntaxes you can choose from to define or refer to a function. Lambda is only one of these.

// lambda assigned to variable
val x: (String) -> Unit = {
    println(it)
}

// anonymous function assigned to variable
val y: (String) -> Unit = fun(input: String) {
    println(input)
}

// reference to existing named function assigned to variable
val z: (String) -> Unit = ::println

// lambda passed to higher order function
“Hello World”.let { println(it) }

// anonymous function passed to higher order function
“Hello World”.let(fun(input: Any) { println(input) })

// reference to existing named function passed to higher order function
“Hello World”.let(::println)

// existing functional reference passed to higher order function
“Hello World”.let(x)

There is actually no such thing as a lambda object that can be passed around. The object is a function that could have been defined using any of the above syntaxes. Once a functional reference exists, the syntax that was used to create it is irrelevant.

With inline higher order functions, as the standard library scope functions are, the compiler optimizes away the creation of the functional object altogether. Of the four higher order calls in my example above, the first three will compile to the same thing. The last is a bit different because the function x already exists so it will be x itself that is invoked in the inlined code. Its contents don’t get hoisted out and called directly in the inlined code.

The advantage of using lambda syntax for higher order inline function calls is that it enables you to use keywords for the outer scope (non-local returns), such as return, continue, or break.

  • Related