はじめに
Kotlinには、functional interfaceという機能があります。
ドキュメントに載っている例をそのまま使って説明すると、functional interfaceとは次のような関数を1つだけ持つインターフェースです。通常のinterfaceと異なる構文上の違いは、interfaceというキーワードの前にfunがついていることです。
fun interface IntPredicate {
fun accept(i: Int): Boolean
}
function interfaceを使うとラムダ式を用いて通常のinterfaceよりも簡易な構文でインスタン化できます。
val isEven = IntPredicate { it % 2 == 0 }
利用の仕方は通常のインターフェースと同じです。
fun main() {
println("Is 7 even? - ${isEven.accept(7)}")
}
この記事では、実体験で気づいたfunctional interfaceが使えないケース(機能しないケース)を紹介したいと思います。
この記事ではほとんど触れませんが、functional interfaceの利点や他の機能との使い分けが気になるかもしれません。その場合は、既にリンク済みのドキュメントやEffective KotlinのUse function types or functional interfaces to pass operations and actionsが参考になると思います。
関数に型パラメータが存在するとき
下記のような型パラメーターを持つ関数は、functional interfaceとして表現できません。
// コンパイルエラー
fun interface ElementSupplier {
fun <T> get(list: List<T>): T
}
代替案は、通常のinterface(先頭のfunを取り除いたもの)を使うことです。
// コンパイルOK
interface ElementSupplier {
fun <T> get(list: List<T>): T
}
関数にレシーバーが付いているとき
Kotlinには、extension functions(拡張関数)と呼ばれる機能があります。
拡張関数はfunctionl interfaceで表現できるでしょうか?
まず定義は問題なくできます。
fun interface IntCalculator {
fun Int.calculate(a: Int) : Int
}
それからインスタンス化もできます。
val sum = IntCalculator { other -> this + other }
ここで問題が発生します。sum
インスタンスのcalculate
関数はどうやって呼び出せばいいのでしょうか?色々と試してみましたが呼び出すことができませんでした。このケースでは、IntCalculator
とInt
の2つのレシーバーが存在しており、外部から呼び出すのはKotlinの言語デザイン上無理な気がします。
こうすれば呼び出せることがわかりました。(でも冗長ですね)
with(sum) {
println(1.calculate(2))
}
代替策は、function literals with receiverを使うことです。
function literals with receiverを使えば上記のsum
の定義は次のように書き換えられます。
val sum: Int.(Int) -> Int = { other -> this + other }
呼び出すには次のようにします。
2.sum(1)
Int.(Int) -> Int
のようなシグニチャがいろんなところに登場して煩雑だということであればtypealiasの出番でしょう。
typealias IntCalculator = Int.(Int) -> Int
上記のtypealiasを使えばsum
の定義は次のように書き換えられます。
val sum: IntCalculator = { other -> this + other }
余談
さらに上記typealiasを使って下記のようなユーティリティ関数を作ってみます。
fun IntCalculator(calculator: IntCalculator): IntCalculator = calculator
この関数を使うと型推論との組み合わせで見た目としてはfunctional interfaceと同等のコードにできます。
val sum = IntCalculator { other -> this + other }
大きな利点はないですが、DSLを作るときなどコードの並び順序を整える時に役立ちます。
おわりに
Kotlinのfunctional interfaceがうまく機能しないケースを紹介しました。
関数にレシーバーがついている時の話は、もっと深い議論(例えばMultiple receivers on extension functions/properties )に関係しているのかもしれません。
議論の内容は追っていないのですが、複数のレシーバーをうまく扱えるのであればDSLの表現の幅が広がるので期待したいところです。