タケハタのブログ

プログラマの生き方、働き方、技術について雑多に書いていくブログです。

Kotlin1.4のKotlinインターフェースに対するSAM変換はなにがいいのか?

先月になりますが、Kotlin 1.4-M1がリリースされました。

blog.jetbrains.com

アップデート内容の中で「More powerful type inference algorithm」にSAM conversion for Kotlin functions and interfaces(Kotlin関数およびインターフェースのSAM変換)というのがあります。
一見すると「SAM変換ってJavaを使うためにあるんじゃないの?」「Kotlinは関数型とtype aliasがあれば関数型インターフェースは不要では?」と考える人もいるのではないかと思います。

なので今回は、KotlinのSAM変換が実際に求められていたケース、使い方を紹介したいと思います。
下記のissueで要望が書かれているので、ここで書かれているコードをもとに説明します。

https://youtrack.jetbrains.com/issue/KT-7770?_ga=2.171160608.1251557499.1576634794-1898058125.1547537046youtrack.jetbrains.com

SAM変換自体については以前に下記のブログでも記事を書いているので、参考までにご覧ください。

blog.applibot.co.jp

Kotlin1.4-M1を使うには

説明の前にまず、Kotlin1.4-M1を動かせるようにします。

IntelliJ IDEAのメニューから Tools -> Kotlin -> Configure Kotlin Plugin Updates を選択します。
そしてUpdate channelで「Early Access Preview 1.4.x」を選択し、Installボタンを押下します。

f:id:take7010:20200423183048p:plain

インストールが終わったらIntelliJ IDEAを再起動し、完了です。

Kotlinのインターフェースに対するSAM変換を使ってみる

Javaの関数型インターフェースに対するSAM変換

通常KotlinでJavaの関数型インターフェースを扱う際は、次のように書くことでSAM変換が適用されます。

val p: Predicate<DoubleArray> = Predicate { true }

java.util.function.Predicatetestという単一引数でbooleanを返却するメソッドを持っているので、

boolean test(T t);

ここではサンプルとして一律trueを返すだけのラムダ式を記述しています。

Javaの関数型インターフェースを継承して使いたい場合

問題になるのが次のケースです。

interface CustomPredicate : Predicate<DoubleArray> {
    // more methods go here
}

これはPredicateを継承したCustomPredicateというKotlinのインターフェースを作っています。
Predicateを継承してカスタマイズした関数型インターフェースとして使いたい場合ですね。

こうするとCustomPredicateは関数型インターフェースと同様の形を成していますが、KotlinのインターフェースとなるためSAM変換は効かなくなります。

interface CustomPredicate: Predicate<DoubleArray>

fun main() {
    // コンパイルエラー
    val cp: Predicate<DoubleArray> = CustomPredicate { true }
}

そして次のように、匿名オブジェクトを書いて渡す必要があります。

val cp: Predicate<DoubleArray> = object : CustomPredicate {
    override fun test(value: DoubleArray): Boolean = true
}

そしてそれならばと、こういったJavaの関数型インターフェースを継承する部分はJavaで作る、という手段を考えてしまいます(Javaで作ればSAM変換が使えるため)。

public interface CustomPredicateJava<T> extends Predicate<T> {
}

主にライブラリやフレームワーク内の、自分では書き換えることができない(Kotlinで作り直すことができない)関数型インターフェースを継承する場合が想定されます。
この問題を解決するために対応されたのが今回のアップデートです。

KotlinでSAMインターフェースを実装する

前述のインターフェースCustomPredicateを、下記のように fun キーワードを付けて定義します。

fun interface CustomPredicate: Predicate<DoubleArray>

このように fun interface という形式でインターフェースを定義すると、Javaの関数型インターフェースと同様にSAMインターフェースとして扱われるようになります。

そしてSAM変換も使えるようになります。

fun interface CustomPredicate: Predicate<DoubleArray>

fun main() {
    val cp: Predicate<DoubleArray> = CustomPredicate { true }
}

ちなみに fun interface として定義した場合、2個目の関数を追加するとコンパイルエラーになります。

// コンパイルエラー
fun interface CustomPredicate : Predicate<DoubleArray> {
    fun hoge()
}

また、Javaの関数型インターフェースは単一のメソッドしか定義されていなければ、 @FunctionalInterface アノテーションを付けて明示しなくてもSAMインターフェースとして認識されましたが、Kotlinは必ず fun が付いていないと認識されないようになっています。

まとめ

Kotlinのコードに対するSAM変換の要望が出てた使いどころは、Javaの関数型インターフェースを継承したインターフェースを作りたい時でした。
Kotlin Blogには使い方だけ書いてあって、具体的な使いどころの説明がなかったので書いてみました。

「More powerful type inference algorithm」の項目に関しては他の部分も追々書こうかなと思います。