kotlinのレシーバー付きラムダについて

kotlinにはレシーバー付きラムダというものがあります。 使い方はこんな感じです。

// レシーバー付きラムダの定義
val lambda: Int.() -> Unit = {
    print(this)
}


// 実行
fun main()
{
    1.lambda()
}

// 出力
// => 1

Int.() -> Unitのような型は、kotlinのドキュメントではFunction types with receiver(レシーバー付き関数型)、kotlinインアクションでは拡張関数型と記述されていました。(以降レシーバー付き関数型)

通常の関数型は(Int) -> Stringのように引数と戻り値の型を指定しますが、レシーバー付き関数型はそれに加えてレシーバーを指定できます。上記のコードで言えばIntがレシーバー型に指定されていて、呼び出し時のレシーバーオブジェクトは1になっています。 拡張関数に似ていますね。

レシーバー付きラムダ{ print(this) }thisでレシーバーオブジェクトを呼び出しています。

レシーバー付きラムダは、ライブラリやフレームワークの中でよく使われています。 一番身近なのはwithrunapplyなどのスコープ関数だと思います。

qiita.com

qiita.com

便利なのですが、レシーバー付きラムダのおかげで定義が少しとっつきづらいので、上記のような解説記事も結構見かけます。

ここではrunの定義におけるレシーバー付きラムダの使われ方を見ていきます。

public inline fun <T, R> T.run(f: T.() -> R): R = f()

runの引数にレシーバー付き関数型のf: T.() -> Rが指定されています。実際に使う時も以下のようにラムダを指定して渡します。

"xxxxxx".run {
   // 処理
}

ただ、自分が初め見たときには、渡されるものがレシーバー付きラムダのはずなのにf()にレシーバーが指定されいないことにひっかかりました。 例えば、runの定義を変えて以下のようにしたらどうなるのでしょうか?(レシーバー付き関数型のレシーバー型をTからStringに変更)

public inline fun <T, R> T.run(f: String.() -> R): R = f()

これは実行できません。 実はrunの定義ではthisが省略されています。runの定義は以下と全く同じです。

public inline fun <T, R> T.run(f: T.() -> R): R = this.f()

kotlinではthisを使えるスコープにある時、レシーバーのthisは省略可能になっています。

ただ、この暗黙的なthisは何がレシーバーになっているのか分かりづらいので個人的にはあまり好きではありません。一般的にスコープ関数もthisを使うものではなくitを使うletalsoの方を用いる方がいいとされています。

おまけ

実はレシーバー付きラムダはこんな使い方もできます。

// レシーバー付きラムダの定義
val lambda: Int.() -> Unit = {
    print(this)
}


// 実行
fun main()
{
    lambda(1)
}

// 出力
// => 1

kotlinの拡張関数やレシーバー付きラムダは、内部的にレシーバーを第一引数に渡す関数になっているらしく、レシーバーに渡すべきものを第一引数に渡しても同じ動きをします。ちょっと面白いですよね。

しかし、拡張関数の場合はレシーバーがないと呼び出しに失敗するので、この方法は使えません。(そもそも使う必要性もありませんが)

// 拡張関数
fun Int.exFun() {
   print(this)
}

// 実行
fun main()
{
    exFun(1)
}

// 出力
// => Unresolved reference: exFun