Kotlin / Programming

ザクっと理解するKotlin Lambda (ラムダ式)とAnonymous Functions(無名関数)

ラムダ式と無名関数は関数リテラル(Function Literals)に分類されます。
関数リテラルとは宣言はされていないが、引数として渡される関数のことを指します。

ラムダ式の書き方

下記はラムダ式のルールです。
・波括弧で囲む
・パラメーターの設定は波括弧内で行い、型の記述は任意
・ボディは -> の右側に書く
・推測される戻り値の型がUnitではない場合、波括弧内の最後の記述が戻り値になる

val abcd: (Int, Int) -> Int = {x: Int, y: Int -> x + y}

このコードをルールに当てはめて考えると
・ラムダ式は波括弧で囲む
  {x: Int, y: Int -> x + y}
・パラメーターの設定は波括弧内で行い、型の記述は任意
  x: Int, y: Int
・ボディは -> の右側に書く
  -> x + y
・推測される戻り値の型がUnitではない場合、波括弧内の最後の記述が戻り値になる
  x + y
となります。

上記は任意の部分も記述しています。除いた場合はこのような記述になります。

val abcd = {x: Int, y: Int -> x + y}

トレーリングラムダ(Trailing lambdas)

ある関数の最後のパラメーターが関数の場合、そのパラメーターとして渡すラムダ式を括弧の外に記述できます。
この書き方をトレーリングラムダ(Trailing lambdas)もしくは末尾ラムダと呼びます。

fun main() {
    val result = calculate(5){x -> x * 2}
    println(result)
}

fun calculate(x: Int, operation: (Int) -> Int): Int {
    return x + operation(2)
}

高階関数のcalculateで第2パラメーターとして関数型を指定しています。
それをmainで呼ぶ際に、calculate(5){x -> x * 2} と括弧の外にラムダ式を記述しています。
これがトレーリングラムダです。

また、引数として渡すものがラムダ式だけの場合は、括弧を省くことができます。

fun main() {
    val result = calculate{x -> x * 2}
    println(result)
}

fun calculate(operation: (Int) -> Int): Int {
    return operation(2)
}

calculate関数のパラメーターは関数型だけです。
この場合、呼び出す際の引数はラムダ式のみになるため括弧は任意になります。

単一パラメーターの暗黙の名前: it

ラムダ式では、パラメーターが1つだけのことが多々あります。

パラメーターが1つだけの場合、パラメータを宣言する必要はなく、-> を省略することができます。パラメータは、暗黙的に it という名前で宣言されます。

下記のように、呼び出し先に関数型を含んだ複数のパラメーターが設定されていても、関数型のパラメーターが複数でなければ it を使用することができます。

・関数のパラメーターが、パラメーターが1つの関数型の場合

fun main() {
    println(function01{it * 2}) // 結果: 10
}

fun function01(operation: (Int) -> Int): Int {
    return operation(5)
}

・関数のパラメーターが、任意の型とパラメーターが1つの関数型の場合

fun main() {
    println(function02(5){it * 2}) // 結果: 15
}

fun function02(x: Int, operation: (Int) -> Int): Int {
    return x + operation(x)
}

・関数のパラメーターが、パラメーターが複数の関数型の場合

fun main() {
    // println(function03{it + it * 2}) // 結果: エラー
}

fun function03(operation: (Int, Int) -> Int): Int {
    return operation(5, 5)
}

この場合、itが第1引数か第2引数を指しているかコンパイラが推測できないため、itは使用できません。
itを使用した場合は下記エラーが表示されます。

Unresolved reference: it
訳: 参照不可: it

こちらのようにitを使わないラムダで表記すればエラーを解消できます。

println(function03{x, y -> x + y * 2}) // 結果: 15

ラムダ式で戻り値を指定

ラムダ式では暗黙的に最後の記述が戻り値として解釈されます。しかし修飾されたreturn文を使うことで明示的に戻り値を指定することができます。

例えばこちらの2つの関数は同じ動きをします。

fun main() {
    val property01 = function01{x: Int ->
        val doubleIt = x * 2
        return@function01 doubleIt + 5
    }
    val property02 = function01{x: Int ->
        val doubleIt = x * 2
        doubleIt + 5
    }
    println(property01) // 結果: 15
    println(property02) // 結果: 15
}

fun function01(operation: (Int) -> Int): Int {
    return operation(5)
}

このように修飾されたreturn文(qualified return)を使用すると明示的に返す値を指定できます。
修飾されたreturn文については別記事にまとめる予定です。

使わないパラメーターにはアンダースコアを

ラムダ式のパラメーターを使用しない場合、任意の名前の代わりにアンダースコア(_)を使用できます。

fun main() {
    val property01 = {_: Int, y: Int -> y}
    println(property01(3, 3)) // 結果 3
}

property01を呼び出す際、第1引数にInt以外の型を指定したり、何も書いていない場合はエラーが表示されます。

無名関数とは

ラムダ式は戻り値の型を明示的に指定することはできず、コンパイラが推論します。
しかし明示的に指定する必要があるときに無名関数を使用します。

例えば、このようにラムダ式で記述した場合、戻り値の型を明示的に指定できません。

val property01 = { x: Int, y: Int -> x + y }

しかし無名関数を使えば戻り値の型を明示的に指定できます。

  val property02 = fun(x: Int, y: Int): Int {return x + y}

無名関数の書き方

無名関数は普通の関数と同じように書きますが、関数名は書きません。
また、ボディはブロックで書くことも、式で書くこともできます。
ただしブロックで書く場合、戻り値の型がUnitではないなら明示的に書く必要があります。

// ブロックで書く場合
val property02 = fun(x: Int, y: Int): Int {return x + y}
    // 下は戻り値の型を指定していないためエラー
    // val property02 = fun(x: Int, y: Int) {return x + y}

// 式で書く場合
val property03 = fun(x: Int, y: Int): Int = x + y

パラメーターの型と戻り値の型をコンパイラが推論できる場合、書く必要はありません。

val property04: (Int, Int) -> Int = fun(x, y) = x + y

ラムダ式と無名関数の違い

上記も含め、ラムダ式と比べると、無名関数にはいくつか違いがあります。
・戻り値の型を明示的に指定できる
・ボディをブロックで記述する場合は、return文が必須
・引数として渡す際は括弧内に記入が必須
・return文はラベル不要

戻り値の型を明示的に指定できる

上記の通りです。

ボディをブロックで記述する場合は、return文が必須

上記の通りです。

引数として渡す際は括弧内に記入が必須

ラムダ式を引数として渡す場合は、括弧の外に書くことができました。
しかし無名関数を引数として渡す場合は、括弧の中に書く必要があります。

fun main() {
    println(function01(){x, y -> x + y})
    println(function01(fun(x: Int, y: Int): Int {return x + y}))
}

fun function01(operation: (Int, Int) -> Int): Int {
    return operation(3, 3)
}

このようにラムダ式を渡す際は、括弧の外にラムダ式を書くことができます。
一方、無名関数の場合は括弧の中に書く必要があります。

return文はラベル不要

ラベルのないreturn文は常に、funキーワードで宣言された関数からリターンします。
つまり、ラムダ式内のreturn文は、それを囲む関数からリターンするのに対して、無名関数内のreturn文は、無名関数自体からリターンします。

fun main() {
    val property01 = function01{x, y ->
        return@function01 x + y
        // return // エラー
    }
    val property02 = function01(fun(x: Int, y: Int): Int {
        return x + y
    })
    
    println(property01)
    println(property02)
}

fun function01(opetation: (x:Int, y: Int) -> Int): Int {
    return opetation(3, 3)
}

ラムダ式内のreturnでラベル「@function01」を取り除くとエラーになります。
ラベルの無いreturn文は一番内側の関数から抜けるため、main関数から抜けることになります。
そのため下記エラーが表示されます。

‘return’ is not allowed here
訳: ここでreturnは許可されていません

無名関数の場合、returnは無名関数自身から抜け出すことになるため問題ありません。

クロージャー

クロージャーは、関数内で宣言された変数やパラメータにアクセスできる範囲のことを指します。
ラムダ式と無名関数はクロージャーにアクセスできるため、外部スコープで宣言された変数や定数を参照できます。

fun main() {
    var ten = 10
    val property01 = function01{x, y ->
        ten += x + y
        return@function01 ten
    }
    var twenty = 20
    val property02 = function01(fun(x: Int, y: Int): Int {
        twenty += x + y
        return twenty
    })
    
    println(property01)
    println(property02)
}

fun function01(opetation: (x:Int, y: Int) -> Int): Int {
    return opetation(3, 3)
}

tenとtwentyはラムダ式と無名関数の外で宣言されていますが、参照や値の変更ができます。

レシーバーを持つ関数リテラル

レシーバーを指定することでコードの読みやすさの向上などの利点があります。
これについては別記事でまとめようと思います。

レシーバーを持つ関数型は、「レシーバーを持つ関数リテラル」という特別な方法でインスタンス化できます。
こちらにもあるように、レシーバーオブジェクトを指定しながら関数型のインスタンスを呼び出すことができます。

fun main() {
    val property01: Int.(Int) -> Int = {x -> this + x}
    println(5.property01(10))
}

コメント

unagi
2023年11月7日 @ 5:58 PM

関数のパラメーターが、任意の型とパラメーターが1つの関数型の場合ですが、
出力が15になるとすれば、
“`
fun function02(x: Int, operation: (Int) -> Int): Int {
return x + operation(2)
}
“`
ではなく
“`
fun function02(x: Int, operation: (Int) -> Int): Int {
return x + operation(x)
}
“`
が正しそうです。
いかがでしょうか。



    2023年11月7日 @ 10:32 PM

    コメントありがとうございます!
    ご指摘いただいた箇所を見直し、間違いに気がつけました
    ありがとうございます
    先ほど修正をいたしました!



コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

新しい言語Mojoとは?

2023年5月13日