Extension-Methoden erlauben Fluent Interfaces in Kotlin, die in Java unmöglich wären. Insbesondere bei generischen Klassen kann man durch Extension-Methoden die gewünschten Typparameter “herauspicken”, um so Methoden nur dann anzubieten, wenn es auch sinnvoll (und typsicher) ist.
Hier ein Beispiel, wo ich eine heterogene Liste (oder konzeptionell eher einen Stack) für Optional
s definiere:
import java.util.*
import java.util.function.Consumer
sealed class Opts<T : Opts<T>> {
fun <E> push(value: Optional<E>): Cons<E, T> {
return Cons(value, self())
}
private
fun self(): T {
@Suppress("UNCHECKED_CAST") //typesafe by CRTP
return this as T
}
object Empty : Opts<Empty>() {
override fun toString() = "EMPTY"
override fun size() = 0
}
abstract fun size(): Int
data class Cons<E, T : Opts<T>>(val head: Optional<E>, val tail: T) : Opts<Cons<E, T>>() {
fun consume(consumer: Consumer<Optional<E>>): T {
consumer.accept(head)
return tail
}
fun peek(consumer: Consumer<Optional<E>>): Cons<E, T> {
consumer.accept(head)
return this
}
fun pop(): T {
return tail
}
fun <F> map(fn: (E) -> F): Cons<F, T> {
return Cons(head.map(fn), tail)
}
fun <F> flatMap(fn: (E) -> Optional<F>): Cons<F, T> {
return Cons(head.flatMap(fn), tail)
}
fun <F, G> split(fn: (E) -> Pair<F, G>): Cons<F, Cons<G, T>> {
val optPair = head.map { fn(it) }
return Cons(optPair.map { it.first }, Cons(optPair.map { it.second }, tail));
}
override fun size() = 1 + tail.size()
}
companion object {
fun empty() = Empty
fun <E> of(value: Optional<E>) = empty().push(value)
fun <E, F> of(v1: Optional<E>, v2: Optional<F>) = empty().push(v1).push(v2)
fun <E, F, G> of(v1: Optional<E>, v2: Optional<F>, v3: Optional<G>) = empty().push(v1).push(v2).push(v3)
}
}
fun <E, F, T : Opts<T>> Opts.Cons<E, Opts.Cons<F, T>>.swap(): Opts.Cons<F, Opts.Cons<E, T>> =
Opts.Cons(this.tail.head, Opts.Cons(this.head, this.tail.tail))
fun <E, F, G, T : Opts<T>> Opts.Cons<E, Opts.Cons<F, T>>.map2(fn: (E, F) -> G): Opts.Cons<G, T> =
Opts.Cons(this.head.flatMap { e -> this.tail.head.map { f -> fn(e, f) } }, this.tail.tail)
fun <E, F, G, T : Opts<T>> Opts.Cons<E, Opts.Cons<F, T>>.flatMap2(fn: (E, F) -> Optional<G>): Opts.Cons<G, T> =
Opts.Cons(this.head.flatMap { e -> this.tail.head.flatMap { f -> fn(e, f) } }, this.tail.tail)
fun <E> Opts.Cons<E, Opts.Empty>.asOptional() = this.head
Interessant sind die Extension-Methoden am Ende. Ich kann die beiden obersten Stack-Elemente nur swappen, wenn der Stack auch mindestens zwei Elemente hat. Die Cons
-Unterklasse könnte in Java “nachschauen”, ob ihr tail
auch wieder ein Cons
ist, aber zum einen wäre das eine Laufzeitprüfung (die swap
-Methode müsste also einen Fehler werfen, wenn der Stack nur ein Element hat), und zum anderen gäbe es keine Möglichkeit, den Rückgabewert richtig zu typen.
Natürlich könnte man das Problem in Java auch mit einer statischen Methode lösen (und die Java API tut auch genau das an einigen Stellen), aber das ist dann eben nicht mehr fluent - und das ist ja hier die ganze Idee hinter der Klasse. Kotlin erlaubt dagegen, typsicher fluent zu bleiben:
val answer = Opts.of(Optional.of("answer"), Optional.of(42))
.swap()
.map2 { x, y -> "$x: $y" }
.asOptional()