Kotlin: Schön flüssig mit Extension-Methoden

kotlin

#1

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 Optionals 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()

#2

Ich habe einen Blog-Beitrag geschrieben, bei dem das Builder-Pattern zählen lernt - auch dank Extension-Methoden:


#3

Da fehlt alles, was zwischen spitzen Klammern steht, oder?


#4

@mrBrown Danke, hab’s korrigiert. Wordpress ist da wirklich verkorkst.