I’ve been doing Kotlin for a while, and I have mixed feelings about extension functions as a feature, but recently I’ve taken advantage of a practice where we define several “versions” of an extension functions in the same package with more-or-less specific receiver types so that the compiler can pick the most appropriate one at compile time. I don’t see this aspect of extensions discussed very much, so I figured I would just post it here in case it’s helpful to someone who hadn’t thought of it before. Keep in mind that this is rarely going to be useful and even when it’s applicable, there’s a good chance it falls in the “premature optimization” category. Now, let me explain:
Hopefully we all know that extension function receiver types are resolved statically, which is quite different from how true method calls works. Here’s an example of exactly what that means:
Regular Polymorphism
open class NumberLike(protected val value: Number) { open fun printMessage() { println("I'm a Number!") } } class IntLike(value: Int): NumberLike(value) { override fun printMessage() { println("I'm an Int!") } } val i: IntLike = IntLike(1) i.printMessage() // prints: "I'm an Int!" val n: NumberLike = IntLike(1) n.printMessage() // prints: "I'm an Int!"
We see that both i
and n
print the same message, even though n
is typed as NumberLike
. That’s because the method is “attached” to the object, itself, and the object is an IntLike
, regardless of whether the compiler knows it at compile time or not.
With extension functions, things work differently:
Extension Function (No Polymorphism)
fun Number.printMessage() { println("I'm a Number!") } fun Int.printMessage() { println("I'm an Int!") } val i: Int = 1 i.printMessage() // Prints: "I'm an Int!" val n: Number = 1 n.printMessage() // Prints: "I'm a Number!"
Notice that the two calls now print different messages. This is because extension functions are not real methods attached to the object, they are just regular functions in disguise. So, there are two separate, unrelated, functions compiled here: one with a signature like (Number) -> Unit
and one with a signature like (Int) -> Unit
. When the compiler is deciding which one to call, it can only go by the variable binding‘s known type at compile time. So, even though n
is actually an Int
, the compiler can’t (generally) know that, so it must call the (Number) -> Unit
function.
This is all well-documented, and even though I hate the feeling of inconsistency it adds to the language, it is what it is. But, there’s something else at work here that’s a little bit interesting that I hadn’t given much thought to: The compiler does pick the most specifically typed function it can at compile time. Since i
is typed as an Int
, it can obviously be used as a Number
as well, yes the compiler is smart enough to favor the (Int) -> Unit
function instead of the (Number) -> Unit
one.
But what happens when there are two options for extension functions, but neither receiver type is a sub-type of the other?
Extensions With Unrelated Receiver Types
interface Foo { } interface Bar { } class FooBar: Foo, Bar fun Foo.printMessage() { println("I'm a Foo!") } fun Bar.printMessage() { println("I'm a Bar!") } val f: Foo = FooBar() f.printMessage() // Prints: "I'm a Foo!" val b: Bar = FooBar() b.printMessage() // Prints: "I'm a Bar!" val fb: FooBar = FooBar() fb.printMessage() // Compile Error: "Overload resolution ambiguity."
So, that’s not surprising. The compiler can’t figure out which extension function it should call because there’s no reason to prefer one over the other; we’ll have to cast fb
to whichever type we want the compiler to use to resolve the extension function it should call.
There are a few places where this technique might make sense. For one hypothetical example, we’ll define a type like Java’s Optional<T> (relax- it’s just an example), called Option<T>
:
sealed interface Option<out T> data class Some<out T>(val value: T): Option<T> object None: Option<Nothing>
To get the value out of an Option
, you’d have to do a type check to see if it’s a Some
, and then get the value from there. But that can be a little tedious. What if we want to just ask an Option
to give us the value if it’s present and give us null
if it’s not? Well, we can’t actually do that for all Option
s, because what if T
is already nullable? Then, if your Option
gives you a null
, you don’t know for sure if the value was “missing” (None
) or if the value was an intentionally stored Some(null)
. So it’s best to define an extension that will only work on Option
s where T
is not nullable:
fun <T: Any> Option<T>.getOrNull(): T? = when (this) { is Some -> value None -> null }
Now, this example is a stretch, because there’s no reason to call getOrNull
after we know that an Option
is actually a Some
or None
, but let’s just pretend because this is getting too long already. If you already know that you have a Some
at a call site and you call getOrNull
, it’s going to do an unnecessary type check. And you can’t implement getOrNull
as a true method with overloads because of the generic type constraint. The only optimization left available to us is static polymorphism:
fun <T: Any> Option<T>.getOrNull(): T? = when (this) { is Some -> value None -> null } fun <T> Some<T>.getOrNull(): T = value // technically we don't even need to constrain T to be non-nullable here because if you know it's a Some, then getting a null just means it was Some(null) fun None.getOrNull(): Nothing? = null // don't need a generic at all. And using Nothing is advisable here because Nothing is a subtype of everything.
So, you get two benefits here if you already proved to the compiler that your Option
is one of its variants:
- You skip a redundant type check
- You get more precise return types. E.g., if you have
Option<Int>
, thengetOrNull()
returnsInt?
, but if you already proved to the compiler that it’s aSome<Int>
, thengetOrNull()
returnsInt
.
I hope that was interesting for someone, because I spent more time typing it than I thought I would. 🙂
Cheers
submitted by /u/ragnese
[link] [comments]