Emulating Static Duck Typing with Kotlin
The other day, I saw someone ask how you can accomplish something like static duck typing in Kotlin. Their background was in C++. I suspect they wanted to know how to accomplish something like this:
class Bird {
public:
auto fly() -> void;
};
class Plane {
auto fly() -> void;
};
template <typename T>
auto fly(T const& obj) -> void {
obj.fly()
}
auto main(int argc, char* argv[]) -> int {
Bird bluejay;
Plane cessna172;
fly(bluejay);
fly(cessna172);
return 0;
}
The idea here that you have two types, that do NOT share a class hierarchy, but still have funtional similarity. In this example, you have birds that can fly and planes that can fly, but birds and planes don’t have anything common otherwise. In the C++ example, you can use standard templates to treat the objects the same in a generic context. C++ generates the code in place at compile time. Kotlin, however, does not and generic functions work entirely differently.
In Kotlin, the way this is typically handled is by creating an interface and implementing that interface in your unrelated objects. However, I discovered that modifying the base classes was impossible. Given that restriction, I think this is probably the best solution that I could come up with:
class Bird {
fun fly() {}
}
class Plane {
fun fly() {}
}
interface Flyable {
fun fly();
}
fun Bird.toFlyable() = object : Flyable {
override fun fly() = this@toFlyable.fly()
}
fun Plane.toFlyable() = object : Flyable {
override fun fly() = this@toFlyable.fly()
}
fun <T : Flyable> fly(obj : T) {
obj.fly()
}
fun main(args: Array<String>) {
val bluejay = Bird()
val cessna172 = Plane()
fly(bluejay.toFlyable())
fly(cessna172.toFlyable())
}
The part that makes this workable is the extension functions that force the
objects into implementing the Flyable
interface. Since Kotlin generics are
just like Java generics, where the generic type is erased at runtime, this is
pretty much the only way you can accomplish this task. I’m sure there’s other
ways, but I think this is probably the safest way. You still have type safety
enforced by the compiler, and you only pay a minimal runtime overhead for the
creation of the object that essentially captures a reference to the original
type and implements the correct interface.
If you look at the bytecode that is generated, it might help to understand how this works with the generic functions:
public final static INNERCLASS toFlyable$1 null null
public final static INNERCLASS toFlyable$2 null null
public final static toFlyable(LBird;)LFlyable;
L0
ALOAD 0
LDC "<this>"
L1
NEW toFlyable$1
DUP
ALOAD 0
INVOKESPECIAL toFlyable$1.<init> (LBird;)V
CHECKCAST Flyable
L2
ARETURN
L3
LOCALVARIABLE $this$toFlyable LBird; L0 L3 0
MAXSTACK = 3
MAXLOCALS = 1
public final static fly(LFlyable;)V
L0
ALOAD 0
LDC "obj"
L1
ALOAD 0
INVOKEINTERFACE Flyable.fly ()V (itf)
L2
RETURN
L3
LOCALVARIABLE obj Flyable; L0 L3 0
MAXSTACK = 2
MAXLOCALS = 1
public final static main()V
L0 NEW Bird
DUP
INVOKESPECIAL Bird.<init> ()V
ASTORE 0
L1
ALOAD 0
INVOKESTATIC toFlyable (LBird;)LFlyable;
INVOKESTATIC fly (LFlyable;)V
L2
RETURN
L3
LOCALVARIABLE bluejay LBird; L1 L3 0
MAXSTACK = 2
MAXLOCALS = 2
I’ve removed a bit of stuff that isn’t entirely relevant, but the functionality is still visible. Inner classes are generated for the objects created by the extension functions, which are then generated by the extension functions and then passed to the type erased generic function.
The byte code for the two extension functions is practically identical, however,
I’m not sure there’s really anything that can be done about it, at least on the
JVM. Could we do the same thing by statically generating the byte code for the
fly
function at compile time? Probably, but I don’t think anyone has proposed
a KEEP to sort it out, and I think the Kotlin creators aren’t really keen on
adding macros or anything like that to the language (even though I’d like to see
some sort of option for generics with structural typing and some sort of macro
system.) Anyway, if you have this issue, and come across this post, I hope this
helps.