ticktakclockの日記

技術ポエムを綴ったりします。GitHub idも同じです (@ticktakclock)

Kotlin applyから理解するレシーバー付きラムダ

こんにちは、tkyです。

『レシーバー付きラムダ』という用語をご存知でしょうか。Kotlinインアクションとかで使われている表現です。

英語では Function literals with receiver と言われています。

今回は apply{} がどのように動作するのか確認しながら『レシーバー付きラムダ』何なのかというものを理解して行きたいと思います。

ラムダ式とは、スコープ関数、拡張関数といった用語の説明はしません。

apply

まず apply をおさらいします。こんな感じでインスタンスを生成したあとにプロパティに値を設定したりとかそんな使い方をするのが一般的ですね。

class User(val name: String) {
    var age: Int = 0
    var email: String = ""
}

val user = User("ticktakclock").apply {
    age = 10
    email = "example@example.com"
}

apply の実装を見てみるとこの様になっています。

public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

注目していただきたいのは関数の引数である block: T.() -> Unit です。 引数にラムダを渡しているわけなのですが、 T.() というように Tの拡張関数のような表現になっていますね。

ここで拡張関数のドキュメントを見に行きます。

kotlinlang.org

以下抜粋

拡張関数を宣言するには レシーバータイプ (receiver type) を関数名の前に付ける必要があります。 次の例では、 swap 関数を MutableList<Int> に追加しています:

fun MutableList<Int>.swap(index1: Int, index2: Int) {
  val tmp = this[index1] // 'this' がリストに対応する
  this[index1] = this[index2]
  this[index2] = tmp
}
拡張関数内での this キーワードは、レシーバオブジェクト(ドットの前に渡されたもの)に対応しています。これで、この関数を任意の MutableList<Int> からでも呼べるようになりました:

はいでました!レシーバー!

この関数を実行(受ける)ときのthisの型またはオブジェクトを レシーバー というのですね。

swap関数をレシーバーはMutableListと理解できます。

block: T.() -> Unit は拡張関数で定義しようとすると次のようになりますが、このときのthisはTですね。別の視点からみてもこの理解でおおよそあってそうです。

fun T.block() {
    // thisはT
}

ラムダ式におけるthisはあくまでそのラムダ式を実行するオブジェクトとなりますが、 レシーバータイプを指定するとことでthisの型を指定することができるということです。

いったん apply のドキュメントを見に行ってみましょう。

kotlinlang.org

コンテキストオブジェクトは、レシーバー(this)として使用できます。

ここでもちゃんとレシーバーが出てきました。

新しい単語(コンテキストオブジェクト)も出てきました。関数のスコープ内で使用する特定のオブジェクトのことを指しているのであっていると思いますが、その特定のオブジェクトにthisでアクセスできるということですね。

続いてラムダ式のリファレンスも見に行ってみましょう。きっと何かが書いてありそうな予感。

kotlinlang.org

ありました。関数型はレシーバーを使用することで関数型のインスタンスを呼び出す事ができる、というようなことが書いてあります。

applyに戻ります。

class User(val name: String) {
    var age: Int = 0
    var email: String = ""
}

val user = User("ticktakclock").apply {
    // applyを呼んだUserインスタンスをレシーバーとしてラムダ式を実行する
    age = 10
    // thisはUserインスタンスなのでthis.age =10 と同じ
    email = "example@example.com"
}

おお〜理解できるかも〜!

まとめ

  • レシーバー付きラムダというのは拡張関数をラムダ式で記述したもののこと
  • レシーバーというのはこの関数を実行(受ける)ときのthisの型またはオブジェクトのこと
  • ドキュメントって何でも書いてあるんだなぁ

理解の仕方や調べ方の助けになったら幸いです。