hydrakecat’s blog

Walking like a cat

Kotlin Inline Functions

これはKotlin Advent Calendar 2015の第8日目の記事です。昨日は n_yunoue さんによることりん、ビッグウェーブに乗るでした。

今日は inline functions の話を書きたいと思います。本題に入る前に function literal と function expression の違いについて説明して、その後に inline modifier やそれにまつわる Kotlin の機能(non-local return、reified modifier や crossline modifier)について説明します。

Difference between Function Literal & Function Expression

Kotlin を書き始めた最初は、function の様々な書き方に戸惑わなかったでしょうか。以下はすべて正しい書き方です。

// Return the longest element
list.maxBy { it.length }
list.maxBy { a -> a.length }
list.maxBy { a: String -> a.length }
list.maxBy (fun(a) = a.length )
list.maxBy (fun(a): Int = a.length )
list.maxBy (fun(a): Int { return a.length })
list.maxBy (fun(a: String): Int { return a.length })

さらに言えば、関数を宣言する方法もいくつかあるのですが、ここでは省略しておきましょう。

さて、標題の function literal と function expression の違いです。上記のどれが function literal で、どれが function expression でしょうか?そうです、ブレース({})に囲まれた関数(最初の3つ)はすべて function literal で、残りの fun キーワードが使われている例は function expression です。簡単ですね。

では、この両者の違いは何でしょうか?実は、両者の違いは return 文の解釈のみに現れるようです*1

例えば、以下のように function literal の中では素の return 文は禁止されており、コンパイルエラーになります。これは、「素の return 文は直近の名前付き関数(named function)か function expression を脱出する」と定められており、lambda からの脱出には使えないからです。return@ordinaryFunction のようなラベルを付ければ、return 文の使用は可能です。

fun foo() {
  ordinaryFunction {
     return // ERROR: can not make `foo` return here
  }
}

fun foo() {
  ordinaryFunction {
     return@ordinaryFunction // OK: exit the lambda
  }
}

一方、function expression や名前付き関数の場合は、このような素の return 文は問題なく使えます。

fun foo() {
  ordinaryFunction (fun() {
     return // OK: exit the function
  )
}

fun foo() {
  ordinaryFunction (fun() {
     return@ordinaryFunction // OK: exit the function
  )
}

この素の return 文の解釈の差については後で触れるので覚えておいてください。

Inline Modifier

本題の inline modifier です。この modifier はご存知の通り、引数で渡された関数をそのままインライン展開することを示します。コードで表すと、以下のようになります。inline modifier が付くと、渡された引数 f を関数オブジェクトとして扱うのではなく、そのまま中身を展開します。

    inline fun inlineFunction(f: () -> Unit) {
        f()
    }

実際にバイトコードでどうなるか確認してみましょう。IntelliJ では Kotlin Bytecode というコマンドを実行すると、Kotlin がどんなバイトコードになるのか簡単に確認できます。特に、Kotlin コード側でポインタを移動すると対応するバイトコードの箇所がハイライトされるのが便利です。

まず、inline modifier が付かない場合。このような Kotlin コードが、

ordinalFunction(fun() {
    println("calling fun")
    return
})

こうなります。

ALOAD 0
GETSTATIC LambdaTest$testLambda02$1.INSTANCE : LLambdaTest$testLambda02$1;
CHECKCAST kotlin/jvm/functions/Function0
INVOKEVIRTUAL LambdaTest.ordinalFunction (Lkotlin/jvm/functions/Function0;)V

ここで LambdaTest$testLambda02$1.INSTANCE というのが無名関数オブジェクトになり、下の方で以下のように定義されます。

// ================LambdaTest$testLambda02$1.class =================
// class version 50.0 (50)
// access flags 0x30
// signature Lkotlin/jvm/internal/Lambda;Lkotlin/jvm/functions/Function0<Lkotlin/Unit;>;
// declaration: LambdaTest$testLambda02$1 extends kotlin.jvm.internal.Lambda implements kotlin.jvm.functions.Function0<kotlin.Unit>
final class LambdaTest$testLambda02$1 extends kotlin/jvm/internal/Lambda  implements kotlin/jvm/functions/Function0  {
  ...
  // access flags 0x11
  public final invoke()V
   L0
    LINENUMBER 36 L0
    LDC "calling fun"
    INVOKESTATIC kotlin/io/ConsoleKt.println (Ljava/lang/Object;)V
   L1
    LINENUMBER 37 L1
    RETURN
   L2
    LOCALVARIABLE this LLambdaTest$testLambda02$1; L0 L2 0
    MAXSTACK = 1
    MAXLOCALS = 1
 ...
}

一方、inline modifier が付くと、以下のような Kotlin コードが、

inlineFunction(fun() {
    println("calling fun in inline function")
    return
})

こうなります。

 LINENUMBER 45 L2
 ALOAD 0
 ASTORE 1
 NOP
L3
 LINENUMBER 9 L3
L4
 LINENUMBER 46 L4
 LDC "calling fun in inline function"
 INVOKESTATIC kotlin/io/ConsoleKt.println (Ljava/lang/Object;)V
L5
 LINENUMBER 47 L5
L6
 GETSTATIC kotlin/Unit.INSTANCE : Lkotlin/Unit;
 POP
L7
 LINENUMBER 10 L7
L8
L9

関数呼び出しが消えて、直接 function expression の中身が実行されているのが分かります。同様の function literal を渡しても同じバイトコードになるのが分かるでしょう。

non-local return

ここで、ようやく、さきほどの return 文の話に戻ります。inline modifier がないとき、以下の Kotlin コードはコンパイルエラーになりました。

fun foo() {
  ordinaryFunction {
     return // ERROR: can not make `foo` return here
  }
}

一方、inline modifier が付いた関数の内部では、このような素の return 文が許されます。ただし、function expression 内の return 文と異なり、外側の foo() メソッドから脱出してしまいます。

fun foo() {
    inlineFunction {
        println("calling lambda in inline function")
        return // OK: exit foo()
    }

    println("never reach here")
}

これが non-local return と呼ばれるものです。少々乱暴に見えるかもしれませんが、たとえば forEach のようなメソッド内で利用すると、通常の for 文と同じような処理ができて便利です。

fun hasZeros(ints: List<Int>): Boolean {
  ints.forEach {
    if (it == 0) return true // returns from hasZeros
  }
  return false
}

なお、この non-local return もバイトコードを見てみると、以下のようになります。println("calling lambda in inline function") の後に RETURN が呼ばれているのが分かります。function expression を引数に渡したときは、ただの POP だったところです。

    LINENUMBER 44 L13
    ALOAD 0
    ASTORE 1
    NOP
   L14
    LINENUMBER 9 L14
   L15
    LINENUMBER 45 L15
    LDC "calling lambda in inline function"
    INVOKESTATIC kotlin/io/ConsoleKt.println (Ljava/lang/Object;)V
   L16
    LINENUMBER 46 L16
    RETURN

noinline modifier

ここからは、すこし細かい inline modifier に関連するいくつかの文法の話です。さきほどの inline function は引数の lambda をすべてインライン展開しましたが、してほしくない場合もあります。そのような場合は noinline modifier を引数の前に指定してやります。

inline fun foo(f: () -> Unit, noinline g: () -> Unit) {
    f()
    g()
}

実際に、以下のように上記の関数を呼び出すと、

foo(fun() {
    println("calling 1st fun in inline function")
    return
}, fun() {
    println("calling 2nd fun in inline function")
    return
})

バイトコードが以下のようになります。2つ目の lambda が関数オブジェクトとして扱われているのが分かります。

    LINENUMBER 40 L2
    ALOAD 0
    ASTORE 1
    GETSTATIC LambdaTest$testLambda$4.INSTANCE : LLambdaTest$testLambda$4;
    CHECKCAST kotlin/jvm/functions/Function0
    ASTORE 2
    NOP
   L3
    LINENUMBER 13 L3
   L4
    LINENUMBER 41 L4
    LDC "calling 1st fun in inline function"
    INVOKESTATIC kotlin/io/ConsoleKt.println (Ljava/lang/Object;)V
   L5
    LINENUMBER 42 L5
   L6
    GETSTATIC kotlin/Unit.INSTANCE : Lkotlin/Unit;
    POP
   L7
    LINENUMBER 14 L7
    ALOAD 2
    INVOKEINTERFACE kotlin/jvm/functions/Function0.invoke ()Ljava/lang/Object;
    POP

crossline modifier

inline function 内で lambda を扱う場合、その lambda をそのまま inline function 内で実行するのではなく、他の文脈で実行する場合があります。その場合は crossline modifier を付けないとコンパイルエラーになります。

    inline fun bar(f: () -> Unit) {
        var g = object: Runnable {
            override fun run() {
                f() // ERROR: Can't inline 'f' here: it may contain non-local returns. Add 'crossinline' modifier to parameter declaration 'f'
            }
        }
    }
    
    inline fun bar(crossline f: () -> Unit) {
        var g = object: Runnable {
            override fun run() {
                f() // OK
            }
        }
    }

reified modifier

inline function では、総称型の具体的な型が確定するため、仮型引数をあたかも実際のクラス型であるかのように扱えます。こう言っても何かよく分からないと思うので実例を見てみましょう。

fun <U> isInstance(t: Any): Boolean {
    return t is U // ERROR: Cannot check for instance of erased type: U
}

inline fun <reified U> isInstance(t: Any): Boolean {
    return t is U // OK
}

Java に慣れている人には周知だと思いますが、Java の総称型はコンパイル時に型引数が決定して型消去が行われるため、ランタイム時には型が分かりません。このため、わざわざ具体的な型を別の引数で指定するなどの苦労をしていました(List.toArray() で配列を別途渡すアレです)。しかし、inline function と、この reified modifier を使うと、そのような苦労から解放されます。素晴しい。現時点では inline function 限定ですが、そのうち他でも使えるようになるのでしょうか。

なお、reified modifier の詳細については、kotlin/reified-type-parameters.md at master · JetBrains/kotlin に書かれています。

まとめ

この記事では、Kotlin の inline modifier とそれに関連する挙動について述べました。特に、

  • non-local return
  • crossline modifier
  • reified modifier

については、inline function 特有なものなので覚えておきましょう。さらに、non-local return については function literal と function expression の違いが重要でした。

Kotlin を学び始めて、まだ一ヶ月の私の感想ですが、Kotlin 的な書き方をしようとすると、どうしても lambda を多用することになります。そのため、パフォーマンスが重要な箇所では inline は必須と言って良いでしょう。

同時に、non-local return の挙動は、私には少々やり過ぎにうつります。まず、function literal と function expression の違いがそこにしかないなら、そのための文法の使い分けは現時点では過剰に思えます。かつ、return 文が lambda からの脱出に見えるので、人によっては予想外の挙動をする可能性があるでしょう。もちろん、inline function でない場合はコンパイラエラーにすることで不用意な return 文は禁止していますが、function literal 内では素の return 文は許さず、ラベルを付けることを必須にしても良かったのではないかと思います。

一方で、reified modifier は inline 限定とはいえ、とてもありがたい機能です。Java プログラミングでは型情報が消去されないように様々なテクニックが使われていますが、そういったものに頼らずに済むのは Java プログラマにとって朗報でしょう。

引き続き Kotlin の勉強をしていきたいと思います。

明日は@RyotaMurohoshiさんによる「Kotlin×Androidではない何か」です。

Reference

*1:少なくとも Kotlin のコンパイラgrep した限りは https://github.com/JetBrains/kotlin/blob/4b8017e34b0842d717df82bbeb1a7f465e8a46fc/compiler/backend/src/org/jetbrains/kotlin/codegen/ExpressionCodegen.java#L1843-L1843 くらいしか大きな影響はないようでした。他にも違いがあることをご存知の方はご指摘いただけたら幸いです。