ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

Kotlin で拡張関数をオーバーライドして実装を切り替えられるぞ!

背景 : コルーチンの Select 式の実装を理解するのが難しかった

コルーチンのドキュメントを読んでいて select 関数というのが出てきたのだけど、これの実装がどうなっているのかすぐにはわからなかった。

suspend fun selectFizzBuzz(fizz: ReceiveChannel<String>, buzz: ReceiveChannel<String>) {
    select<Unit> { // <Unit> means that this select expression does not produce any result 
        fizz.onReceive { value ->  // this is the first select clause
            println("fizz -> '$value'")
        }
        buzz.onReceive { value ->  // this is the second select clause
            println("buzz -> '$value'")
        }
    }
}
kotlinx.coroutines/coroutines-guide.md at master · Kotlin/kotlinx.coroutines · GitHub

fizz.onReceiveSelectClause1<E>のプロパティなのだけど、その後ろのラムダが何なのかぱっと見はわからなかったのである。 (SelectClause1<E> 型には invoke は定義されていない。)

select 関数のシグネチャは以下。 引数の関数型はレシーバ付きで、レシーバの型は SelectBuilder である。

public inline suspend fun <R> select(crossinline builder: SelectBuilder<R>.() -> Unit): R

この SelectBuilder を見ると、下記のような拡張関数が定義されていた。 これらが fizz.onReceive { /* ... */ } の実体なのであった。

public operator fun <Q> SelectClause1<Q>.invoke(block: suspend (Q) -> R)

しかも SelectBuilderインターフェイスで、拡張関数は別のクラスで実装されていた。

学び

拡張関数をオーバーライドできる

Extensions declared as members can be declared as open and overridden in subclasses.

Extensions - Kotlin Programming Language

拡張関数をメンバーとして定義できるのは知ってたのだけど、オーバーライドできるとは知らなかった!

interface StringExtensionScope {
    fun String.bar(): String
}

class StringExtensionScopeImpl : StringExtensionScope {
    override fun String.bar() = this + " bar"
}

そして実行時に実装を変更できる

拡張関数を定義した型をレシーバとするレシーバ付きの関数型を使うと、ラムダ内をスコープにして拡張関数を有効にできる! 実行時に実装を変更することも可能!

val printFooBar: StringExtensionScope.() -> Unit = {
    // StringExtensionScope で定義されている拡張関数を利用できる。
    println("foo".bar())
}

// そして実行時に拡張関数の実装を変更できる。
printFooBar(StringExtensionScopeImpl())

拡張関数といえば 「静的に解決されるものである」 という印象だったのでオーバーライドして実行時に実装を切り替えられるとは思っていなかったけど実際は切り替えることができる。 ちなみに拡張レシーバ (extension receiver) の型はやはり静的に見られるので、拡張レシーバの型に応じて動的に実装を切り替えるということはできない。

This means that the dispatch of such functions is virtual with regard to the dispatch receiver type, but static with regard to the extension receiver type.

Extensions - Kotlin Programming Language

終わり

小ネタだけど、拡張関数は常に静的に解決されるものだと思ってたのでちょっとびっくりした。

Kotlin も拡張関数と operator とレシーバ付き関数型が組み合わさってくると結構コード追いづらくなるなーと感じる。 (まあ Scala とかと比べるとまだまだ追いやすい方だとは思うけど。)