【Kotlin】高階関数、ラムダ式・無名関数、クロージャに関する備忘録

Kotlin の関数まわりのことを学習したので、備忘録として残しています。

Kotlin では「関数」も「第一級オブジェクト」

第一級オブジェクトとは、簡単に言うと、Int 型や String 型などの型と同じように扱えるオブジェクトのことです。

具体的には、第一級オブジェクトに対しては、以下のような操作が可能です。

第一級オブジェクト
  • 変数に代入できる
  • 関数の引数や戻り値に指定できる

例えば、以下のような関数「createCamelCase」を定義します。

//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
}

 

関数からは、関数オブジェクトを生成することができ、以下のように 変数に代入したり、関数の引数に渡したりすることができます。

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 関数は高階関数)

 

ラムダ式

上記の例では、関数オブジェクトを生成する方法 として、以下の手順を踏みました。

定義した関数から関数オブジェクトを取得
  1.  関数を定義する(createCamelCase)
  2.  定義した関数から、関数オブジェクトを取得する(::createCamelCase)

しかし、使い捨ての関数などに、わざわざ名前をつけて宣言してやる必要はないかもしれません。

このような、名前を持たない関数オブジェクトを直接生成する方法がラムダ式無名関数です。

ラムダ式の例

createCamelCase を、ラムダ式で表現して printResult 関数に渡してみます。

//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」を明示しているため、以下のように省略できます。

//型を省略
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」型の関数オブジェクトを取るため、以下のような書き方ができます。

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 関数を、無名関数で書き換えてみます。

    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
    })

クロージャ

クロージャとは何か??

まずは結論から!(私の現時点での認識です...)

クロージャとは?
  • クロージャとは、簡単に言うと ラムダ式や無名関数などの関数オブジェクトのことを指す。
  • クロージャには、「関数オブジェクトの外にある変数を補足し続ける」という性質がある。

具体例で見ていきます。

本来、関数の中で宣言した変数は、その関数が実行し終わるとアクセスできなくなります。

fun main(args : Array){

    fun incrementCount(){
        var count = 0
        count++
        println(count)
    }

    incrementCount() // 1 が出力。
    incrementCount() // 1 が出力。変数 count は incrementCount 関数を実行するたびにリセットされ、何度実行しても結果は 1 になる。
}

 

変数 count は incrementCount 関数を実行するたびにリセットされ、何度実行しても結果は 1 になります。

incrementCount() 関数の実行が終わっても、変数 count を捕捉し続ける方法はないでしょうか?

あります!それがクロージャを使う方法です。

fun main(args : Array){

    // 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 の書籍や公式情報を読むことが多いです。

随時更新やアウトプットしていきたいと思います( ^ω^ )