【Kotlin】高階関数、ラムダ式・無名関数、クロージャに関する備忘録
Kotlin の関数まわりのことを学習したので、備忘録として残しています。
Kotlin では「関数」も「第一級オブジェクト」
第一級オブジェクトとは、簡単に言うと、Int 型や String 型などの型と同じように扱えるオブジェクトのことです。
具体的には、第一級オブジェクトに対しては、以下のような操作が可能です。
- 変数に代入できる
- 関数の引数や戻り値に指定できる
例えば、以下のような関数「createCamelCase」を定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//str1 と str2 を連結して、キャメルケースにする関数 fun createCamelCase(str1 : String, str2 : String) : String{ var result = str1.toLowerCase() when { str2.length > 1 -> result += str2[0].toUpperCase() + str2.drop(1).toLowerCase() str2.length == 1 -> result += str2[0].toUpperCase() } return result } |
関数からは、関数オブジェクトを生成することができ、以下のように 変数に代入したり、関数の引数に渡したりすることができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
fun main(args : Array){ //関数オブジェクト f を第三引数に取る printResult 関数(高階関数)を定義。 fun printResult(str1 : String, str2 : String, f : (String,String) -> String){ println(f(str1, str2)) } //関数名の前に「::」をつけることで関数オブジェクトを取得できる val fObj = ::createCamelCase //printResult 関数の引数に、関数オブジェクト(fObj)を渡している printResult("MAKE","ROBOT", fObj) //-> 実行した結果、"MAKE" と "ROBOT" が連結して、"makeRobot" 文字列が出力される。 } |
なお、関数には型があります。
上記の例では、printResult 関数の第三引数として、createCamelCase 関数の型「(String, String) -> String」を指定しています。
「(String, String) -> String」型で表される関数は、引数として String 型を 2つ取り、String 型を1つ返す、という意味になります。
- Kotlin では、関数オブジェクトを変数に代入したり、別の関数の引数や戻り値に指定できる。(第一級オブジェクト)
- 引数や戻り値に関数オブジェクトを取る関数を、高階関数と呼ぶ。(上記の例では、printResult 関数は高階関数)
ラムダ式
上記の例では、関数オブジェクトを生成する方法 として、以下の手順を踏みました。
- 関数を定義する(createCamelCase)
- 定義した関数から、関数オブジェクトを取得する(::createCamelCase)
しかし、使い捨ての関数などに、わざわざ名前をつけて宣言してやる必要はないかもしれません。
このような、名前を持たない関数オブジェクトを直接生成する方法が、ラムダ式や無名関数です。
ラムダ式の例
createCamelCase を、ラムダ式で表現して printResult 関数に渡してみます。
1 2 3 4 5 6 7 8 9 10 11 |
//printResult 関数の引数に、「(String, String) -> String 」型のラムダ式を渡している。 printResult("MAKE", "ROBOT", {str1 : String, str2 : String -> var result = str1.toLowerCase() when { str2.length > 1 -> result += str2[0].toUpperCase() + str2.drop(1).toLowerCase() str2.length == 1 -> result += str2[0].toUpperCase() } result }) |
- 波括弧{} の中に、ラムダ式を記述する。(「 {引数リスト -> 本文} 」という構文)
- 戻り値の型を指定しない。(コンパイラが自動で推論できる場合が多いため)
- return 文は使用せず、ラムダ式の末尾に記述した値が評価され、戻り値となる。(上記例では result )
ただ、上記のコードは少し冗長です。
実際のラムダ式は、引数の型も省略 されていることが多いです。(コンパイラが型を推論できる場合)
今回のケースでは、printResult 関数の定義部分に、引数に取る関数オブジェクトの型 「(String, String) -> String」を明示しているため、以下のように省略できます。
1 2 3 4 5 6 7 8 9 10 11 12 |
//型を省略 printResult("MAKE", "ROBOT", {str1,str2 -> var result = str1.toLowerCase() when { str2.length > 1 -> result += str2[0].toUpperCase() + str2.drop(1).toLowerCase() str2.length == 1 -> result += str2[0].toUpperCase() } result }) |
さらに、高階関数の最後の引数に関数オブジェクトを取る場合、より書きやすい書き方(シンタックスシュガー)が用意されています。
printResult 関数は、最後の引数に「(String, String) -> String」型の関数オブジェクトを取るため、以下のような書き方ができます。
1 2 3 4 5 6 7 8 9 10 11 |
printResult("MAKE", "ROBOT"){str1,str2 -> var result = str1.toLowerCase() when { str2.length > 1 -> result += str2[0].toUpperCase() + str2.drop(1).toLowerCase() str2.length == 1 -> result += str2[0].toUpperCase() } result } |
本来なら、引数は ( ) の中に記述する必要がありますが、これを外に出すことができます。
さらにさらに、ラムダ式の引数が1つだけの場合、引数名も省略することができます。引数名を省略した場合、その引数は it という名前で参照することが可能です。
今回の例では、String 型を2つ(str1, str2)、引数に取っているため、省略はできません。。
- ラムダ式は、関数オブジェクトを直接生成するための方法。
- ラムダ式は名前を持たない。
- 戻り値の型は明示しない。また、引数の型も省略される場合が多い。
- Kotlin の標準ライブラリなどでも、最後の引数に関数オブジェクトを取るパターンが多いため、特別な書き方(シンタックスシュガー)が用意されている。
- ラムダ式の引数が1つだけの場合、引数名も省略できる。省略した場合、引数には it で参照できる。
無名関数
無名関数も、ラムダ式と同様で、関数オブジェクトを生成するための構文です。
機能的には無名関数もラムダ式もほとんど同じですが、以下のような違いがあります。
- 戻り値の型を指定する。(ラムダ式では不要)
- 戻り値は return 文で返す。(ラムダ式では、関数末尾の値が自動的に戻り値となって返される)
- 非ローカルリターン(非局所リターン)が利用できない。(ラムダ式では利用可能。本記事では取り扱いません。詳細はこちら)
以下の公式情報にあるように、戻り値の型は指定する必要がない場合が多いです。どうしても戻り値の型を指定したい場合や、無名関数の構文が好きな場合は、ラムダ式ではなく無名関数を使うことになるでしょう。
無名関数
上記のラムダ式の構文から一つ欠落しているのは、関数の戻り値の型を指定する機能です。ほとんどの場合は、戻り型を自動的に推論することができるので不要です。しかし、それを明示的に指定する必要がある場合、別の構文を使用することができます。_無名関数_です。
引用:https://dogwood008.github.io/kotlin-web-site-ja/docs/reference/lambdas.html
先ほど例示した createCamelCase 関数を、無名関数で書き換えてみます。
1 2 3 4 5 6 7 8 9 10 |
printResult("MAKE", "ROBOT", fun(str1, str2) : String{ var result = str1.toLowerCase() when { str2.length > 1 -> result += str2[0].toUpperCase() + str2.drop(1).toLowerCase() str2.length == 1 -> result += str2[0].toUpperCase() } return result }) |
クロージャ
クロージャとは何か??
まずは結論から!(私の現時点での認識です...)
- クロージャとは、簡単に言うと ラムダ式や無名関数などの関数オブジェクトのことを指す。
- クロージャには、「関数オブジェクトの外にある変数を補足し続ける」という性質がある。
具体例で見ていきます。
本来、関数の中で宣言した変数は、その関数が実行し終わるとアクセスできなくなります。
1 2 3 4 5 6 7 8 9 10 11 |
fun main(args : Array<String>){ fun incrementCount(){ var count = 0 count++ println(count) } incrementCount() // 1 が出力。 incrementCount() // 1 が出力。変数 count は incrementCount 関数を実行するたびにリセットされ、何度実行しても結果は 1 になる。 } |
変数 count は incrementCount 関数を実行するたびにリセットされ、何度実行しても結果は 1 になります。
incrementCount() 関数の実行が終わっても、変数 count を捕捉し続ける方法はないでしょうか?
あります!それがクロージャを使う方法です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
fun main(args : Array<String>){ // incrementCount で宣言した変数 count の値を +1 する「関数オブジェクト(クロージャ)」を返す。 fun incrementCount() : () -> Unit{ var count = 0 return { count++ println(count) } } var f1 = incrementCount() var f2 = incrementCount() f1() // 1 が出力 f1() // 2 が出力。関数オブジェクト (クロージャ) が変数 count を補足し続けているため、count の値を変更できる。 f2() // 1 が出力。f1() とは別メモリ上の変数 count を補足している。 f2() // 2 が出力 } |
一般的なクロージャの説明は、こちらがわかりやすいです。
おわりに
最近は自分の中で Kotlin 熱が上がっており、プライベート学習で Kotlin の書籍や公式情報を読むことが多いです。
随時更新やアウトプットしていきたいと思います( ^ω^ )