Kotlin で関数型とラムダ式を使用する

1. はじめに

この Codelab では、関数型、関数型の使用方法、ラムダ式に固有の構文について説明します。

Kotlin では、関数はファースト クラスの構造体とみなされます。つまり、関数はデータ型として扱うことができます。関数を変数に格納し、引数として他の関数に渡して、他の関数から返すことができます。

リテラル値で表現できる他のデータ型(10 値の Int 型と "Hello" 値の String 型など)と同様に、ラムダ式または簡略化してラムダと呼ばれる関数リテラルを宣言することもできます。ラムダ式は、Android 開発や Kotlin プログラミングで広く使用されます。

前提条件

  • 関数、if/else ステートメント、null 可能性など、Kotlin プログラミングに精通していること

学習内容

  • ラムダ構文を使用して関数を定義する方法。
  • 関数を変数に格納する方法。
  • 関数を引数として他の関数に渡す方法。
  • 他の関数から関数を返す方法。
  • null 値許容の関数型を使用する方法。
  • ラムダ式をより簡潔にする方法。
  • 高階関数の概要。
  • repeat() 関数を使用する方法。

必要なもの

  • Kotlin プレイグラウンドにアクセスできるウェブブラウザ

2. Code-Along 動画を見る(省略可)

コースの講師が Codelab を完了する様子を視聴する場合は、以下の動画を再生してください。

Kotlin のプレイグラウンドとコードを見やすくするために、動画を全画面表示(動画の隅にあるこのアイコン 正方形の 4 つの角が強調表示されたこの記号は、全画面モードを表します。 を使用して)に拡張することをおすすめします。

このステップは省略可能です。動画をスキップして、すぐに Codelab の学習を開始することもできます。

3. 関数を変数に格納する

ここまでのところで、fun キーワードを使用して関数を宣言する方法を学びました。fun キーワードで宣言した関数は呼び出すことができ、それによって関数本体のコードが実行されます。

ファースト クラスの構造体である関数はデータ型でもあるため、関数を変数に格納する、関数に渡す、さらに関数から返すことができます。以前の Codelab で行ったように、ランタイムにアプリの動作を変更する、またはコンポーズ可能な関数をネストして、レイアウトをビルドすることが必要になる場合があります。これはすべて、ラムダ式によって行うことができます。

この動作を、トリック オア トリートの例を通じて見ていきます。トリック オア トリートとは、コスチュームを着た子どもたちが家々を回って「トリック オア トリート」と言い、そのお返しに通常はお菓子をもらえる、多くの国でのハロウィーンの伝統的な習慣です。

関数を変数に格納します。

  1. Kotlin プレイグラウンドに移動します。
  2. main() 関数の後で、パラメータと戻り値のない、"No treats!" を出力する trick() 関数を定義します。構文は、前の Codelab で使用した他の関数の構文と同じです。
fun main() {

}

fun trick() {
    println("No treats!")
}
  1. main() 関数の本体で、trickFunction という変数を作成し、trick に設定します。関数を呼び出すのではなく、変数に格納するため、trick の後にかっこは挿入しません。
fun main() {
    val trickFunction = trick
}

fun trick() {
    println("No treats!")
}
  1. コードを実行します。Kotlin コンパイラは tricktrick() 関数の名前として認識しますが、変数に割り当てるのではなく、関数を呼び出すことを想定しているため、エラーが発生します。
Function invocation 'trick()' expected

trickFunction 変数に trick を格納しようとしました。しかし、関数を値として参照するには、関数参照演算子(::)を使用する必要があります。この構文を以下の図に示します。

a9a9bfa88485ec67.png

  1. 関数を値として参照するには、trickFunction::trick に再割り当てします。
fun main() {
    val trickFunction = ::trick
}

fun trick() {
    println("No treats!")
}
  1. コードを実行して、エラーが発生しなくなったことを確認します。trickFunction は使用しないでくださいという警告が表示されますが、次のセクションで解決します。

ラムダ式を使用して関数を再定義する

ラムダ式は、fun キーワードのない関数を定義する簡潔な構文を備えています。別の関数で関数を参照せずに、ラムダ式を変数に直接格納できます。

代入演算子(=)の前に、val キーワードまたは var キーワードを追加し、その後に変数名を挿入します。この変数名は、関数を呼び出すときに使用します。代入演算子(=)の後ろにはラムダ式があります。これは、関数の本体を囲む中かっこのペアで構成されます。この構文を以下の図に示します。

5e25af769cc200bc.png

ラムダ式を使用して関数を定義する場合は、関数を参照する変数を設定します。他の型と同様に、値を他の変数に割り当てて、新しい変数名で関数を呼び出すこともできます。

ラムダ式を使用するようにコードを更新します。

  1. ラムダ式を使用して trick() 関数を書き換えます。名前 trick は変数の名前を参照するようになりました。中かっこの関数本体はラムダ式になりました。
fun main() {
    val trickFunction = ::trick
}

val trick = {
    println("No treats!")
}
  1. trick は関数名ではなく変数を参照するようになったため、main() 関数で関数参照演算子(::)を削除します。
fun main() {
    val trickFunction = trick
}

val trick = {
    println("No treats!")
}
  1. コードを実行します。エラーは発生せず、関数参照演算子(::)を使用することなく trick() 関数を参照できます。関数をまだ呼び出していないため、出力は表示されません。
  2. main() 関数で、trick() 関数を呼び出しますが、今回は他の関数を呼び出す場合と同じように、かっこを挿入します。
fun main() {
    val trickFunction = trick
    trick()
}

val trick = {
    println("No treats!")
}
  1. コードを実行します。ラムダ式の本文が実行されます。
No treats!
  1. main() 関数で、trickFunction 変数を関数のように呼び出します。
fun main() {
    val trickFunction = trick
    trick()
    trickFunction()
}

val trick = {
    println("No treats!")
}
  1. コードを実行します。この関数は 2 回呼び出されます(trick() 関数の呼び出しに対して 1 回、2 回目は trickFunction() 関数の呼び出しに対して呼び出されます)。
No treats!
No treats!

ラムダ式を使用すると、関数を格納する変数を作成し、これらの変数を関数のように呼び出して、関数のように呼び出せる他の変数に格納できます。

4. 関数をデータ型として使用する

前の Codelab で、Kotlin には型推論があることを学びました。多くの場合、変数を宣言する際に、型を明示的に指定する必要はありません。前述の例では、Kotlin コンパイラは trick の値が関数であると推測できました。ただし、関数パラメータの型や戻り値の型を指定する場合は、関数型を表現するための構文について把握しておく必要があります。関数型は、オプションのパラメータ リスト、-> 記号、戻り値の型を含むかっこのセットで構成されています。この構文を以下の図に示します。

5608ac5e471b424b.png

前に宣言した trick 変数のデータ型は () -> Unit です。この関数にはパラメータがないため、かっこは空です。この関数は何も返さないため、戻り値の型は Unit です。2 つの Int パラメータを受け取り、Int を返す関数がある場合、そのデータ型は (Int, Int) -> Int です。

関数の型を明示的に指定するラムダ式を使用して、別の関数を宣言します。

  1. trick 変数の後に、"Have a treat!" を出力する本文を持つラムダ式と等しい treat という変数を宣言します。
val trick = {
    println("No treats!")
}

val treat = {
    println("Have a treat!")
}
  1. treat 変数のデータ型を () -> Unit として指定します。
val treat: () -> Unit = {
    println("Have a treat!")
}
  1. main() 関数で、treat() 関数を呼び出します。
fun main() {
    val trickFunction = trick
    trick()
    trickFunction()
    treat()
}
  1. コードを実行します。treat() 関数は trick() 関数と同様に動作します。明示的に宣言しているのは treat 変数のみですが、どちらの変数も同じデータ型です。
No treats!
No treats!
Have a treat!

関数を戻り値の型として使用する

関数はデータ型であるため、他のデータ型と同様に使用できます。他の関数から関数を返すこともできます。この構文を以下の図に示します。

f16dd6ca0c1588f5.png

関数を返す関数を作成します。

  1. main() 関数からコードを削除します。
fun main() {

}
  1. main() 関数の後に、Boolean 型の isTrick パラメータを受け入れる trickOrTreat() 関数を定義します。
fun main() {

}

fun trickOrTreat(isTrick: Boolean): () -> Unit {
}

val trick = {
    println("No treats!")
}

val treat = {
    println("Have a treat!")
}
  1. trickOrTreat() 関数の本体で、isTricktrue の場合は trick() 関数を返し、isTrick が false の場合は treat() 関数を返す if ステートメントを追加します。
fun trickOrTreat(isTrick: Boolean): () -> Unit {
    if (isTrick) {
        return trick
    } else {
        return treat
    }
}
  1. main() 関数で、treatFunction という変数を作成して、trickOrTreat() の呼び出し結果に代入し、isTrick パラメータに false を渡します。次に、trickFunction という 2 つ目の変数を作成し、trickOrTreat() の呼び出し結果に代入します。今回は、isTrick パラメータに true を渡します。
fun main() {
    val treatFunction = trickOrTreat(false)
    val trickFunction = trickOrTreat(true)
}
  1. treatFunction() を呼び出してから、次の行で trickFunction() を呼び出します。
fun main() {
    val treatFunction = trickOrTreat(false)
    val trickFunction = trickOrTreat(true)
    treatFunction()
    trickFunction()
}
  1. コードを実行します。各関数の出力が表示されます。trick() 関数または treat() 関数を直接呼び出していない場合でも、これらの関数を呼び出すことができます。これは、trickOrTreat() 関数を呼び出すごとに戻り値を保存し、trickFunction 変数と treatFunction 変数を使用して関数を呼び出したためです。
Have a treat!
No treats!

これで、関数がどのように他の関数を返すかがわかりました。関数を引数として別の関数に渡すこともできます。2 つの文字列のいずれかを返す以外の処理を実行するカスタムの動作を trickOrTreat() 関数に設定する必要が生じることもあります。ある関数を引数として受け取る関数は、呼び出されるたびに別の関数を渡すことができます。

関数を引数として別の関数に渡す

ハロウィーンを祝う世界の一部の地域では、子どもたちはお菓子の代わりに小銭を受け取ります。または、両方を受け取ります。trickOrTreat() 関数を変更して、関数で表される追加のもてなしを引数として指定できるようにします。

trickOrTreat() がパラメータとして使用している関数も、独自のパラメータを受け取る必要があります。関数型を宣言する場合、パラメータにはラベルが付加されません。必要なのは、各パラメータのデータ型をカンマで区切って指定することのみです。この構文を以下の図に示します。

8372d3b83d539fac.png

パラメータを受け取る関数のラムダ式を作成すると、パラメータには生成された順に名前が付けられます。パラメータ名は開いた中かっこの後にリストされます。それぞれの名前はカンマで区切られます。パラメータ名と関数本体は矢印(->)で区切られます。この構文を以下の図に示します。

938d2adf25172873.png

関数をパラメータとして受け取るように trickOrTreat() 関数を更新します。

  1. isTrick パラメータの後に、(Int) -> String 型の extraTreat パラメータを追加します。
fun trickOrTreat(isTrick: Boolean, extraTreat: (Int) -> String): () -> Unit {
  1. else ブロックで、return ステートメントの前に println() を呼び出し、extraTreat() 関数の呼び出しを渡します。5extraTreat() の呼び出しに渡します。
fun trickOrTreat(isTrick: Boolean, extraTreat: (Int) -> String): () -> Unit {
    if (isTrick) {
        return trick
    } else {
        println(extraTreat(5))
        return treat
    }
}
  1. これで、trickOrTreat() 関数を呼び出す場合に、ラムダ式を使用して関数を定義し、extraTreat パラメータに渡すことが必要になります。main() 関数で、trickOrTreat() 関数を呼び出す前に coins() 関数を追加します。coins() 関数は、Int パラメータに quantity という名前を付け、String を返します。キーワード return は存在しないため、ラムダ式では使用できません。代わりに、関数内の最後の式の結果が戻り値になります。
fun main() {
    val coins: (Int) -> String = { quantity ->
        "$quantity quarters"
    }

    val treatFunction = trickOrTreat(false)
    val trickFunction = trickOrTreat(true)
    treatFunction()
    trickFunction()
}
  1. 以下に示すように、coins() 関数の後に cupcake() 関数を追加します。Int パラメータ quantity に名前を付け、-> 演算子を使用して関数本体から分離します。これで、coins() 関数または cupcake() 関数を trickOrTreat() 関数に渡すことができます。
fun main() {
    val coins: (Int) -> String = { quantity ->
        "$quantity quarters"
    }

    val cupcake: (Int) -> String = { quantity ->
        "Have a cupcake!"
    }

    val treatFunction = trickOrTreat(false)
    val trickFunction = trickOrTreat(true)
    treatFunction()
    trickFunction()
}
  1. cupcake() 関数で、quantity パラメータと -> 記号を削除します。これらは使用されていないため、省略できます。
val cupcake: (Int) -> String = {
    "Have a cupcake!"
}
  1. trickOrTreat() 関数の呼び出しを更新します。最初の呼び出しで、isTrickfalse の場合は、coins() 関数を渡します。2 番目の呼び出しでは、isTricktrue の場合に cupcake() 関数を渡します。
fun main() {
    val coins: (Int) -> String = { quantity ->
        "$quantity quarters"
    }

    val cupcake: (Int) -> String = {
        "Have a cupcake!"
    }

    val treatFunction = trickOrTreat(false, coins)
    val trickFunction = trickOrTreat(true, cupcake)
    treatFunction()
    trickFunction()
}
  1. コードを実行します。extraTreat() 関数は、isTrick パラメータが false 引数に設定されている場合にのみ呼び出されるため、出力には 5 クオーターが含まれますが、カップケーキは含まれません。
5 quarters
Have a treat!
No treats!

null 値許容の関数型

他のデータ型と同様に、関数型は null 値許容型として宣言できます。このような場合、変数には関数を含めるか、null にします。

関数を null 値許容型として宣言するには、関数型をかっこで囲み、閉じかっこの後ろに ? 記号を付加します。たとえば、() -> String 型を null 値許容型にする場合は、(() -> String)? 型として宣言します。この構文を以下の図に示します。

c8a004fbdc7469d.png

trickOrTreat() 関数を呼び出すたびに extraTreat() 関数を指定しなくても済むように、extraTreat パラメータを null 値許容にします。

  1. extraTreat パラメータの型を (() -> String)? に変更します。
fun trickOrTreat(isTrick: Boolean, extraTreat: ((Int) -> String)?): () -> Unit {
  1. extraTreat() 関数の呼び出しを変更し、if ステートメントを使用して、その関数が null でない場合にのみ関数を呼び出すようにします。trickOrTreat() 関数は次のコード スニペットのようになります。
fun trickOrTreat(isTrick: Boolean, extraTreat: ((Int) -> String)?): () -> Unit {
    if (isTrick) {
        return trick
    } else {
        if (extraTreat != null) {
            println(extraTreat(5))
        }
        return treat
    }
}
  1. cupcake() 関数を削除し、trickOrTreat() 関数への 2 回目の呼び出しで cupcake 引数を null に置き換えます。
fun main() {
    val coins: (Int) -> String = { quantity ->
        "$quantity quarters"
    }

    val treatFunction = trickOrTreat(false, coins)
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
}
  1. コードを実行します。出力は変更されません。関数型を null 値許容型として宣言できるようになったため、extraTreat パラメータに関数を渡す必要はなくなりました。
5 quarters
Have a treat!
No treats!

5. 簡単な構文でラムダ式を記述する

ラムダ式には、コードをより簡潔にするためのさまざまな方法が用意されています。取り上げているラムダ式の多くは省略した構文で記述されているため、このセクションではその一部を見ていきます。

パラメータ名を省略する

coins() 関数を記述した際に、関数の Int パラメータに quantity という名前を明示的に宣言しました。ただし、cupcake() 関数で説明したように、パラメータ名を完全に省略できます。関数にパラメータが 1 つあり、名前を指定しない場合、Kotlin は暗黙的に it 名を割り当てます。これにより、パラメータ名と -> 記号を省略でき、ラムダ式がより簡潔になります。この構文を以下の図に示します。

332ea7bade5062d6.png

パラメータに省略形の構文を使用するように coins() 関数を更新します。

  1. coins() 関数で、quantity パラメータ名と -> 記号を削除します。
val coins: (Int) -> String = {
    "$quantity quarters"
}
  1. $it を使用して、1 つのパラメータを参照するように "$quantity quarters" 文字列テンプレートを変更します。
val coins: (Int) -> String = {
    "$it quarters"
}
  1. コードを実行します。Kotlin は Int パラメータの it パラメータ名を認識し、引き続きクオーター数を出力します。
5 quarters
Have a treat!
No treats!

ラムダ式を関数に直接渡す

現在、coins() 関数は 1 つの場所でのみ使用されます。最初に変数を作成せずにラムダ式を trickOrTreat() 関数に直接渡すことができるとしたらどうでしょう。

ラムダ式は単に、0 が整数リテラルであるか、"Hello" が文字列リテラルであるのと同様に、関数リテラルです。ラムダ式は関数呼び出しに直接渡すことができます。この構文を以下の図に示します。

39dc1086e2471ffc.png

coins 変数を削除できるようにコードを変更します。

  1. ラムダ式を移動して、trickOrTreat() 関数の呼び出しに直接渡すようにします。ラムダ式を 1 行に集約することもできます。
fun main() {
    val coins: (Int) -> String = {
        "$it quarters"
    }
    val treatFunction = trickOrTreat(false, { "$it quarters" })
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
}
  1. coins 変数は使用されなくなったため、削除します。
fun main() {
    val treatFunction = trickOrTreat(false, { "$it quarters" })
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
}
  1. コードを実行します。引き続き想定どおりにコンパイルされて実行されます。
5 quarters
Have a treat!
No treats!

後置ラムダ構文を使用する

関数の型が関数の最後のパラメータの場合は、別の省略形を使用してラムダを書き込むことができます。その場合は、ラムダ式を閉じかっこの後に配置すると、関数を呼び出すことができます。この構文を以下の図に示します。

3ee3176d612b54.png

これにより、ラムダ式が他のパラメータから分離されるため、コードが読みやすくなりますが、コードの動作は変わりません。

後置ラムダ構文を使用するようにコードを更新します。

  1. treatFunction 変数で、trickOrTreat() の呼び出しの閉じかっこの後にラムダ式 {"$it quarters"} を移動します。
val treatFunction = trickOrTreat(false) { "$it quarters" }
  1. コードを実行します。すべて正常に動作します。
5 quarters
Have a treat!
No treats!

6. repeat() 関数を使用する

関数が関数を返すか、関数を引数として受け取る場合は、高階関数と呼ばれます。trickOrTreat() 関数は高階関数の例です。パラメータとして ((Int) -> String)? 型の関数を受け取り、() -> Unit 型の関数を返すためです。Kotlin には、ラムダの新しい知識を活用できる、便利な高階関数がいくつか用意されています。

repeat() 関数は、そのような高階関数の一つです。repeat() 関数を使用すると、for ループを関数で簡潔に表現できます。この関数や他の高階関数は、後のユニットで頻繁に使用します。repeat() 関数には次のような関数署名があります。

repeat(times: Int, action: (Int) -> Unit)

times パラメータは、アクションを実行する必要がある回数です。action パラメータは単一の Int パラメータを受け取り、Unit 型を返す関数です。action 関数の Int パラメータは、最初の反復処理の 0 引数や 2 番目の反復処理の 1 引数など、これまでにアクションが実行された回数です。repeat() 関数を使用すると、for ループと同様に、コードを指定された回数だけ繰り返すことができます。この構文を以下の図に示します。

519a2e0f5d02687.png

repeat() 関数を使用することで、trickFunction() 関数を 1 回だけではなく、複数回呼び出せます。

トリック オア トリートのコードを更新して、repeat() 関数の動作を確認します。

  1. main() 関数で、treatFunction()trickFunction() の呼び出しの間に repeat() 関数を呼び出します。times パラメータに 4 を渡し、action 関数に後置ラムダ構文を使用します。ラムダ式の Int パラメータの名前を指定する必要はありません。
fun main() {
    val treatFunction = trickOrTreat(false) { "$it quarters" }
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
    repeat(4) {

    }
}
  1. treatFunction() 関数の呼び出しを repeat() 関数のラムダ式に移動します。
fun main() {
    val treatFunction = trickOrTreat(false) { "$it quarters" }
    val trickFunction = trickOrTreat(true, null)
    repeat(4) {
        treatFunction()
    }
    trickFunction()
}
  1. コードを実行します。"Have a treat" 文字列は 4 回出力されます。
5 quarters
Have a treat!
Have a treat!
Have a treat!
Have a treat!
No treats!

7. まとめ

お疲れさまでした。関数型とラムダ式の基礎を学習しました。Kotlin 言語について理解を深める際に、これらのコンセプトを理解しておくと役立ちます。また、関数型、高階関数、簡略構文を使用することで、コードがより簡潔になり、読みやすくなります。

まとめ

  • Kotlin の関数はファースト クラスの構造体であり、データ型と同様に扱うことができます。
  • ラムダ式には、関数を作成するための簡単な構文が用意されています。
  • 関数型は他の関数に渡すことができます。
  • 関数型は別の関数から返すことができます。
  • ラムダ式は最後の式の値を返します。
  • 1 つのパラメータを持つラムダ式でパラメータ ラベルが省略されている場合は、it 識別子によって参照されます。
  • ラムダは変数名を付けずにインラインで記述できます。
  • 関数の最後のパラメータが関数型の場合は、後置ラムダ構文を使用して、関数呼び出し時に最後のかっこの後にラムダ式を移動できます。
  • 高階関数とは、他の関数をパラメータとして受け取る関数または関数を返す関数です。
  • repeat() 関数は、for ループと同様に動作する高階関数です。

詳細