Thursday 2 March 2023

Kotlin asSequence

Kotlin is a really amazingly rich language and when I thought I had managed to internalise those of its features that I had never (or very rarely) used before in other languages (lambda expressions with receiver, qualified returns, inline functions, etc) I've just come across again some code that had me scratching my head for a while thinking "what the fuck is that code doing?".

The difference between Iterables and Sequences is very interesting (it's not new, you have lazy collection operations in .Net Linq, in JavaScript lodash, in python map-filter...) but the way kotlin allows you switching from eager to lazy by wrapping your collection in a class implementing the Sequence interface is pretty nice. The typical methods to operate on collections (filter, map, all, any, takeWhile...) are implemented as extension functions in both the Iterable and Sequence interfaces. Those in the Iterable interface work egarly and those in the Sequence interface work lazily. These methods rely on an Iterator to obtain elements from the collection, but they call iterator.next eiter eagerly (those from Iterator) or lazily (those from Sequence). So obtaining a Sequence from a Collection should be just getting an object that implements the Sequence interface and which iterator() method returns the iterator of that Collection. Looking into the implementation of asSequence I came across this (documented as: "Creates a Sequence instance that wraps the original array returning its elements when being iterated"):


public fun  Array.asSequence(): Sequence {
    if (isEmpty()) return emptySequence()
    return Sequence { this.iterator() }
}


So, understanding what's happening there in the return Sequence { this.iterator() } is what motivates this post. That line is creating an object of an anonymous class that implements the Sequence interface. The Sequence interface has a single non default method (it's a SAM - Functional Interface) so you can pass a function literal as the implementation of that single method (as explained here). So I'm creating an object of a new anonymous class that implements the Sequence interface, (that has only a method, iterator). That class uses the code that we have defined in the lambda (and that just creates an iterator on the current object) as implementation for its single method. Notice that the lambda is a closure that is trapping as "this" the current this, I mean, the object on which we invoke asSequence (so in the end this "this" will be a field of the anonymous class (normally something like "this$0").)

I've disassemblied the compiled code (see the next paragraph), and to my surprise in this case the Kotlin compiler is treating the lambda the same way the Java compiler manages lambdas, through the invokedynamic bytecode instruction. This means that the kotlin compiler instead of creating the class that I have just described, it just emits an invokedynamic instruction, and it will be at runtime when the class is created. This is well explained for example here

The Kotlin documentation describes this behaviour:

SAM adapters via invokedynamic

Kotlin 1.5.0 now uses dynamic invocations ( invokedynamic ) for compiling SAM (Single Abstract Method) conversions:

Over any expression if the SAM type is a Java interface
Over lambda if the SAM type is a Kotlin functional interface

The new implementation uses LambdaMetafactory.metafactory() and auxiliary wrapper classes are no longer generated during compilation

As for the disassemblied code, here it is. Notice that I'm using the amazing ByteCode Viewer application, and it directly shows the Bootstrap Method (BSM) code just next to the invokedynamic instruction).

invokedynamic java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; : run(LfunctionalInterfaces/Person;)Ljava/lang/Runnable; ()V functionalInterfaces/Person.runRunnable$lambda$0(LfunctionalInterfaces/Person;)V (6) ()V

No comments:

Post a Comment