高階関数とラムダ
Kotlinでは関数はファーストクラスです、 つまり関数を変数やデータ構造に格納したり、そのほかの高階関数に引数で渡したりそこからreturnされたり出来ます。 関数以外の変数に対して出来るような演算をいろいろ関数にも行う事が出来ます。
Kotlinは静的に型づけされる言語なので、 これらの機能を使いやすくする為に、 関数を表す為の一連の関数の型を使ったり、 ラムダ式などのこの用途に特化したような言語機能を提供しています。
高階関数
高階関数はパラメータとして関数を取るか、関数を返す関数です。
高階関数の良い例としては、コレクションに対する
関数型言語のイディオムであるfold
が挙げられます。
foldはaccumulator(訳注:結果を累積的に保持するようなもの)の初期値と畳み込み関数(combining function)を引数にとり、
コレクションの要素に順番にaccumulatorの現在の値と要素の値で畳み込んで、結果の値でaccumulatorを更新する、
を繰り返して、最終的な値を返します:
fun <T, R> Collection<T>.fold(
initial: R,
combine: (acc: R, nextElement: T) -> R
): R {
var accumulator: R = initial
for (element: T in this) {
accumulator = combine(accumulator, element)
}
return accumulator
}
上のコードで、combine
パラメータは関数の型 (R, T)->R
を持ちます。
つまり、二つの引数で型がそれぞれR
とT
のものを受け取りR
を返す関数を受け入れます。
それはfor
ループの中で呼び出され、
その戻り値はaccumulator
に代入されています。
fold
を呼ぶためには、関数の型の何らかのインスタンスを引数にわたす必要があります。
高階関数の呼び出し側ではこの目的にラムダ式(詳細は以下で説明する)が広く使われています:
関数の型
Kotlinは (Int) -> String
のような関数の型を使用して、
関数に関連する宣言などを扱います: val onClick: () -> Unit = ...
。
これらの型は関数のシグニチャ、つまりパラメータと戻りの値に対応した、特別な記法となっています:
-
すべての関数の型にはカッコ書きされたパラメータの型のリストと戻りの型があります:
(A, B) -> C
は、 型A
とB
の二つの引数を取り、型C
の値を返す関数を表す。() -> A
のように、パラメータの型リストは空の場合もある。Unit
が戻り値の型の場合にも省略は出来ない。 -
関数の型は追加で レシーバ の型を持つ事も出来ます。レシーバの型はドットの前に置きます: 型、
A.(B) -> C
は、レシーバオブジェクトの型がA
に対して呼べる関数で、パラメータがB
、戻りの型がC
のものを表します。 レシーバ付き関数リテラルはこの種の型と合わせて良く使われます。 -
Suspend関数(未翻訳)は特別な種類の関数の型になっていて、表記にsuspend修飾子があります。 例えば
suspend () -> Unit
やsuspend A.(B) -> C
といった風です。
関数の型の記法は関数のパラメータの名前を含める事も出来ます: (x: Int, y: Int) -> Point
など。
これらの名前はパラメータの意味を表すドキュメント的な役割を果たす場合があります。
関数の型がnullableであると指定する為には、
カッコを以下のように使います: ((Int, Int) -> Int)?
関数の型はカッコをつかって組み合わせる事もできます: (Int) -> ((Int) -> Unit)
アロー記法は右結合です。
(Int) -> (Int) -> Unit
は先程の例と同じ意味であって、((Int) -> (Int)) -> Unit
ではありません。
関数の型にTypeエイリアスを使って別の名前を与える事も出来ます:
typealias ClickHandler = (Button, ClickEvent) -> Unit
関数の型のインスタンス生成
関数の型のインスタンスを得るには幾つかの方法があります:
- 以下の形式のどれかの関数リテラルの中にコードブロックを書く事で
レシーバ付き関数リテラルはレシーバ付きの関数の型の値として使う事が出来ます。
- 既存の宣言の呼び出し可能リファレンス(callable reference)を使う事で:
- トップレベル、ローカル、メンバ、または拡張の関数:
::isOdd
,String::toInt
, - トップレベル、ローカル、メンバ、または拡張の プロパティ:
List<Int>::size
, - コンストラクタ:
::Regex
これらには特定のインスタンスのメンバを指すような、束縛された呼び出し可能リファレンスも含みます:
foo::toString
. - トップレベル、ローカル、メンバ、または拡張の関数:
- 関数の型をインターフェースとして実装しているカスタムクラスのインスタンスを使う事で:
class IntTransformer: (Int) -> Int {
override operator fun invoke(x: Int): Int = TODO()
}
val intFunction: (Int) -> Int = IntTransformer()
十分な情報があれば、コンパイラは変数の持つ関数の型を推論出来ます:
val a = { i: Int -> i + 1 } // 推論される型は (Int) -> Int
レシーバ有りとレシーバ無しの、リテラルで無い関数の型の値は相互に交換可能です。
レシーバが最初の引数になったり、その最初の引数がレシーバになる事によって。
例えば、型 (A, B) -> C
の値は、A.(B) -> C
が期待されている所に渡したり代入したり出来ますし、
反対も可能です:
デフォルトの推論ではレシーバ無しの方が選ばれます、たとえ拡張関数のリファレンスで変数を初期化してもです。 この挙動を変えたければ、変数に明示的に型を指定しましょう。
関数の型のインスタンスの呼び出し
関数の型の値は、invoke(...)
演算子を用いる事で実行出来る:
f.invoke(x)
や、単に f(x)
とする事で。
値がレシーバ型を持つなら、レシーバオブジェクトは最初の引数として渡される必要があります。
レシーバを持つ関数の型を呼ぶそれ以外の方法としては、レシーバーオブジェクトを前に置いて拡張(extension)関数のように呼ぶというのも出来ます: 1.foo(2)
例:
インライン関数
高階関数を使う時に、 インライン関数を使うとより柔軟な制御フローを使えるようになります。 この事が便利な事が時々あります。
ラムダ式と無名関数
ラムダ式や無名関数は「関数リテラル」です。すなわち、その関数は宣言されるのではなく、式としてすぐに渡されるということです。次の例を考えてみます:
max(strings, { a, b -> a.length < b.length })
関数 max
は高階関数です。すなわち2番目の引数として関数値をとります。この2番目の引数はそれ自体が関数である式、すなわち関数リテラルです。関数としては、次の名前付き関数と等価です:
fun compare(a: String, b: String): Boolean = a.length < b.length
ラムダ式の構文
ラムダ式の完全な構文形式は、次のとおりです。
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
- ラムダ式は常に中括弧で囲まれる。
- 完全な構文形式のパラメータ宣言は中カッコ内にあり、オプショナルな型注釈を持つことができる。
- 本体は
->
記号の後に置かれる。 - ラムダの推論された戻りの型が
Unit
の場合を除いて、ラムダの本体の最後の(場合によってはたった一つの)式は戻りの値として扱われる。
必須ではない注釈をすべて省略した場合、残ったものは次のようになります:
val sum = { x: Int, y: Int -> x + y }
トレーリングラムダを渡す
(訳注:Passing trailing lambdas)
Kotlinの規約により、関数の最後のパラメータが関数だった場合、 その対応する引数としてラムダ式を渡す場合はカッコの外側に置く事が出来ます:
val product = items.fold(1) { acc, e -> acc * e }
そのような構文はトレーリングラムダ(trailing lambda)という名前でも知られています。
もしラムダが呼び出しの唯一の引数の場合、カッコそのものも含めて省略する事も出来ます:
run { println("...") }
it
: 単一パラメータの暗黙の名前
ラムダ式がパラメータを1つしか持っていないことはよくあることです。
もしコンパイラがパラメータ無しのシグニチャをパースすることができるケースなら、
パラメータを宣言する必要は無く、->
も省略出来ます。
パラメータは暗黙のうちに it
という名で宣言されます。
ints.filter { it > 0 } // このリテラルは '(it: Int) -> Boolean' 型
ラムダ式から値を返す
限定子付きreturn構文を使えば、 ラムダから明示的に値を返す事が出来ます。 それ以外の場合では、最後の式の値が暗黙的に返されます。
つまり、以下の二つのコード片は等価です:
ints.filter {
val shouldFilter = it > 0
shouldFilter
}
ints.filter {
val shouldFilter = it > 0
return@filter shouldFilter
}
この規約と、カッコの外にラムダ式を渡すを組み合わせると、 LINQスタイル のコードを書くことができます:
strings.filter { it.length == 5 }.sortedBy { it }.map { it.uppercase() }
使わない変数のアンダースコア
ラムダのパラメータが使われない時は、名前の代わりにアンダースコアを使う事が出来ます:
map.forEach { (_, value) -> println("$value!") }
ラムダにおけるdestructuring
ラムダにおけるdestructuringは、分解宣言(destructuring declaration)に解説されています。
無名関数
上記のラムダ式の構文から一つ欠落しているのは、関数の戻り値の型を指定する機能です。ほとんどの場合は、戻り型を自動的に推論することができるので不要です。しかし、それを明示的に指定する必要がある場合、別の構文を使用することができます。無名関数です。
fun(x: Int, y: Int): Int = x + y
無名関数は、その名が省略されていることを除いて、通常の関数の宣言と非常によく似ています。 その本体は、式(上記のように)、またはブロックのいずれかになります:
fun(x: Int, y: Int): Int {
return x + y
}
パラメータおよび戻り型は、通常の関数と同じ方法で指定されますが、 文脈からパラメータの型を推測出来る場合がある所が違います:その場合はパラメータの型を省略することができます。
ints.filter(fun(item) = item > 0)
無名関数の戻り値の型推論は普通の関数のように動作します:
無名関数の関数本体が式の時は戻りの型は自動的に推論され、
無名関数の巻数本体がブロックの時は明示的に指定されなくてはいけません(無ければ Unit
と想定されます)。
無名関数をパラメータとして渡す時は、常にかっこ内に渡されることに注意してください。 括弧の外に関数を残すことができる簡略シンタックスは、ラムダ式に対してのみ機能します。
ラムダ式と無名関数の間のもう一つの違いは、非局所的なリターンの動作です。 ラベルなしの return 文は、常に fun キーワードで宣言された関数からreturnします。 これは、ラムダ式の内側からの return は囲んでいる関数からreturnする一方で、 無名関数の内部 return は無名関数自体からreturnすることを意味します。
クロージャ
ラムダ式や無名関数(ならびにローカル関数やobject式)は、その クロージャ 、すなわち、外側のスコープで宣言された変数にアクセスすることができます。 クロージャに取り込まれた変数をラムダで変更することができます:
var sum = 0
ints.filter { it > 0 }.forEach {
sum += it
}
print(sum)
レシーバ付き関数リテラル
レシーバのある関数の型、つまり A.(B) -> C
のようなものは、
特別な形の関数リテラルでインスタンス化出来ます。
それはレシーバ付き関数リテラルという形式です。
さきに述べた通り、 Kotlinは、レシーバ付きの関数の型のインスタンスを呼ぶ機能を提供していて、 この時にはレシーバオブジェクトを(訳注:呼び出される関数オブジェクトに)提供します。
リテラル関数の本体内では、呼び出しに渡されるレシーバオブジェクトは暗黙の this
となり、
任意の追加の修飾子なしでそのレシーバオブジェクトのメソッドを呼び出したり、
this
式を使ってレシーバオブジェクトにアクセスしたり出来ます。
これは、関数の本体内でレシーバオブジェクトのメンバにアクセスすることを可能にする拡張関数に似ています。
以下はレシーバ付き関数リテラルに型指定をつけた例で、plus
はレシーバオブジェクトの物が呼ばれています:
val sum: Int.(Int) -> Int = { other -> plus(other) }
無名関数の構文は、直接関数リテラルのレシーバの型を指定することができます。 これはレシーバを持つ関数の型の変数を宣言し、後でそれを使用する必要がある場合に役立ちます。
val sum = fun Int.(other: Int): Int = this + other
ラムダ式は、レシーバの型を文脈から推測することができる場合、レシーバ付き関数リテラルとして使用することができます。 それらの使用法の最も重要な例の一つは、タイプセーフビルダーです。
class HTML {
fun body() { ... }
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML() // レシーバオブジェクトを生成
html.init() // そのレシーバオブジェクトをラムダに渡す
return html
}
html { // レシーバ付きラムダがここから始まる
body() // レシーバオブジェクトのメソッドを呼んでいる
}