5

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:

  • generics, in the example above, but can't get the syntax right.
  • sealed classes, but I don't have control of these classes.
  • union types, which I've found aren't supported in Kotlin.
  • wrapper classes, but couldn't get the syntax right.
  • passing lambdas a la this blog explanation, but didn't get it right, and it seemed boptimalsu to pass multiple lambdas around for every method.

There must be a better way, right?

sirksel
  • 747
  • 6
  • 19

2 Answers2

3

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}")
}
Alex
  • 7,460
  • 2
  • 40
  • 51
  • I tried your code, and then tried to eliminate None/null... Can I `throw` instead of `Calc.None` to make the `object None` unnecessary? It seemed to work, but will that cause problems with the typechecker down the line? Is it unKotlinic to not have a None? Thanks. – sirksel Nov 14 '17 at 07:38
  • 1
    @sirksel I'm not a big expert in Kotlin, but since this implementation don't have any restrictions on which type extension can be applied to, I'd say it's better to fall back to default implementation rather than throw. But if you want to get rid of Default object you can probably just create a specialized extensions for String List and Sequence like you did in OP. – Alex Nov 14 '17 at 07:56
  • Are you saying you'd repeat the identical extension functions for `dbl` in each of String List and Sequence... as an alternative to all of your code? Or are you saying that I'd change the code you wrote by replacing just the overridden functions (or something else) with extensions? I'm still learning this... thanks. – sirksel Nov 14 '17 at 08:46
  • 1
    @sirksel I've updated my answer to illustrate what I mean. Cheers – Alex Nov 14 '17 at 08:52
1

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.

Abhijit Sarkar
  • 21,927
  • 20
  • 110
  • 219