Kotlinのレシーバ付き関数リテラルとは

Kotlinにはレシーバ付き関数リテラルと呼ばれるものがあります。
私自身としては、単語自体は聞いたことがあるけれど、それが何者なのかは分からないといった状態が続いていました。

本記事では、各用語の意味を整理しながら、レシーバ付き関数リテラルについて説明していきます。


関数リテラルとは何か

まず、関数リテラルについて見ていきたいと思います。
余談ですが、私自身は数年前に新卒研修で初めてJavaについて学んだときにリテラルという単語を知りましたが、当時は意味を全く理解できなかった記憶だけが残っています😇

リテラルの意味については、e-Wordsから引用させていただきます。

リテラルとは、コンピュータプログラムのソースコードなどの中に、特定のデータ型の値を直に記載したもの。また、そのように値をコードに書き入れるために定められている書式。


例えば、以下のコードにおいて、1は整数リテラル"hello"(※helloではありません)は文字列リテラルと呼ばれます。

val num = 1
val text = "hello"


同様にして、関数リテラルとは、関数型の値を直に記載したもの、およびその書式という意味になります。

関数オブジェクト

「関数型の値を直に記載したもの」は関数オブジェクトと言い換えることができます。

Kotlinでは、無名関数ラムダ式などを使って関数オブジェクトを生成できます。
各詳細については公式ドキュメントをご参照ください。

無名関数による関数オブジェクトの生成

無名関数による関数オブジェクトの生成は以下のように行います。

val sum = fun(num1: Int, num2: Int): Int {
  return num1 + num2
}


ラムダ式による関数オブジェクトの生成

ラムダ式による関数オブジェクトの生成は以下のように行います。

val sum = { num1: Int, num2: Int ->
  num1 + num2
}


レシーバとは何か

関数リテラルについて整理できたので、次はレシーバについて見ていきたいと思います。

レシーバとは、拡張関数の宣言時に関数名の前に付与する型、およびその拡張関数を実行するオブジェクトのことで、前者はレシーバ型、後者はレシーバオブジェクトと呼ばれます。

以下のコードでは、関数名sumの前のIntがレシーバ型、1がレシーバオブジェクトとなります。
拡張関数内では、thisキーワードによってレシーバオブジェクトを参照できます

// 拡張関数の宣言
fun Int.sum(num: Int): Int {
  return this + num
}

// 拡張関数の実行
1.sum(2) // => 3


また、以下の2つの関数はシグネチャ(①関数名、②パラメータの数、③各パラメータの型の3つの組み合わせ)が同一の関数として認識されます。
つまり、拡張関数はレシーバオブジェクトを第1引数とする関数と等価です。

fun Int.sum(num: Int): Int {
  return this + num
}

fun sum(num1: Int, num2): Int {
  return num1 + num2
}


レシーバ付き関数リテラルとは何か

ここまでの内容から、レシーバ付き関数リテラルとは、レシーバ付きの関数型の値を直に記載したもの、およびその書式と言って差し支えないと思います。
また、「レシーバ付きの関数型の値を直に記載したもの」は拡張関数の関数オブジェクトの生成と言い換えることができます。

拡張関数も関数なので、関数オブジェクトを生成できます。
注意すべき点として、拡張関数はレシーバオブジェクトを第1引数とする関数と等価であるため、拡張関数の関数オブジェクトを代入する変数や関数のパラメータには型を明示する必要があります

無名関数による拡張関数の関数オブジェクトの生成は以下のように行います。

// 拡張関数の関数オブジェクトの生成
val sum: Int.(Int) -> Int = fun Int.(num): Int {
  return this + num
}

// 通常の関数オブジェクトの生成(型を明示しなければこの型として推論されます)
val sum: (Int, Int) -> Int = fun(num1, num2): Int {
  return num1 + num2
}


同様に、ラムダ式による拡張関数の関数オブジェクトの生成は以下のように行います。

// 拡張関数の関数オブジェクトの生成
val sum: Int.(Int) -> Int = { num ->
  this + num
}

// 通常の関数オブジェクトの生成(型を明示しなければこの型として推論されます)
val sum: (Int, Int) -> Int = { num1, num2 ->
  num1 + num2
}


レシーバ付き関数リテラルの使い方

レシーバ付き関数リテラルについて整理できたので、レシーバー付き関数リテラルの使い方について見ていきたいと思います。

実は、Kotlinを使っている方の多くはレシーバー付き関数リテラルを無意識のうちに使っていると思います。
例えば、スコープ関数apply()を用いた実装を行うときに記述するラムダ式はレシーバ付き関数リテラルです。

apply()の内部実装は以下のようになっています。

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


また、apply()を使うときは以下のようになります。このときに記述するラムダ式がレシーバ付き関数リテラルです。
apply()T.() -> Unit型の関数オブジェクトを受け取っています。

"hello".apply {
  println("this: $this")
  // => this: hello
}


最後に、apply()が受け取るレシーバ付き関数リテラルの中でapply()のレシーバオブジェクトをthisで参照できる理由が少し分かりづらいと感じたので、それを説明して終わりたいと思います。

apply()にはT.() -> Unit型のblock()というパラメータがあり、関数内でblock()を実行しています。
block()T型の拡張関数なので、省略されているthis、すなわちapply()のレシーバオブジェクトに対して実行されます。
このため、apply()が受け取るレシーバ付き関数リテラルの中でapply()のレシーバオブジェクトをthisで参照できます。