sirksel
sirksel

Reputation: 787

Polymorphism on extension functions in Kotlin

I have a several classes I don't control, upon which I've already created several identically-named extension methods across several common "attributes". The identically-named extension functions always return the same value type, though calculated in different ways for each type of receiver. Here is a simplified example based on built-in types for just one attribute:

// **DOES NOT COMPILE**

// three sample classes I don't control extended for .len
inline val String.len get() = length
inline val <T> List<T>.len get() = size
inline val <T> Sequence<T>.len get() = count()

// another class which needs to act on things with .len
class Calc<T>(val obj:T) {       // HERE IS THE PROBLEM...
  val dbl get() = obj?.len * 2   // dummy property that doubles len
  // ... and other methods that use .len and other parallel extensions 
}

fun main(a:Array<String>) {
  val s = "abc"
  val l = listOf(5,6,7)
  val q = (10..20 step 2).asSequence()
  val cs = Calc(s)
  val cl = Calc(l)
  val cq = Calc(q)
  println("Lens:  ${cs.dbl}, ${cl.dbl}, ${cq.dbl}")
}

Imagine several other "common" properties extended in the same manner as .len in some classes I don't control. If I don't want to repeat myself in every class, how do I construct a properly typed class that can operate on .len (and other such properties) generically for these three classes?

I've researched the following but not found workable solutions yet:

There must be a better way, right?

Upvotes: 5

Views: 1867

Answers (2)

Abhijit Sarkar
Abhijit Sarkar

Reputation: 24518

Extensions are resolved statically.

This means that the extension function being called is determined by the type of the expression on which the function is invoked, not by the type of the result of evaluating that expression at runtime.

I personally think this design decision limits the usability of extensions, since we can't invoke the function by subtype polymorphism. Currently, there is no other way than to use ad-hoc polymorphism by overloading extension functions and accept the intended receiver as a parameter, as shown in this answer. Then the JVM chooses the appropriate overloaded function to call at runtime based on the receiver argument type.

There's another way to solve this problem, using type classes; instead of defining len on each type, you define an interface Lengthy that has method length(), define conversions for each specific type to Lengthy, and invoke someLengthy.length(). See this article for details. However, the article uses Scala which supports implicit conversions, Kotlin doesn't, so the code won't be as succinct.

Upvotes: 2

Alex
Alex

Reputation: 7926

Here's a example with sealed classes and a single extension property to convert anything to something which can give you len or double. Not sure if it has better readability thogh.

val Any?.calc get() = when(this) {
    is String -> Calc.CalcString(this)
    is List<*> -> Calc.CalcList(this)
    is Sequence<*> -> Calc.CalcSequense(this)
    else -> Calc.None
}

/* or alternatively without default fallback */

val String.calc get() = Calc.CalcString(this)
val List<*>.calc get() = Calc.CalcList(this)
val Sequence<*>.calc get() = Calc.CalcSequense(this)

/* sealed extension classes */

sealed class Calc {

    abstract val len: Int?

    val dbl: Int? by lazy(LazyThreadSafetyMode.NONE) { len?.let { it * 2 } }

    class CalcString(val s: String): Calc() {
        override val len: Int? get() = s.length
    }

    class CalcList<out T>(val l: List<T>): Calc() {
        override val len: Int? get() = l.size
    }

    class CalcSequense<out T>(val s: Sequence<T>): Calc() {
        override val len: Int? get() = s.count()
    }

    object None: Calc() {
        override val len: Int? get() = null
    }

}

fun main(args: Array<String>) {
    val s = "abc".calc
    val l = listOf(5,6,7).calc
    val q = (10..20 step 2).asSequence().calc

    println("Lens:  ${s.dbl}, ${l.dbl}, ${q.dbl}")
}

Upvotes: 3

Related Questions