コレクションを操作する高階関数

1. はじめに

Kotlin で関数型とラムダ式を使用する Codelab で、高階関数について学びました。これは、他の関数をパラメータとして受け取ったり、関数を返したりする、repeat() などの関数です。高階関数は特にコレクションと深い関係があり、並べ替えやフィルタリングなどの一般的なタスクをより少ないコードで実行するのに役立ちます。コレクションを扱うための基礎を十分に習得したところで、もう一度高階関数について学びましょう。

この Codelab では、コレクション型に対して使用できるさまざまな関数(forEach()map()filter()groupBy()fold()sortedBy() など)を学習します。その過程で、ラムダ式を扱う追加の演習を行います。

前提条件

  • 関数型とラムダ式に精通していること。
  • 後置ラムダ構文(repeat() 関数など)に精通していること。
  • Kotlin のさまざまなコレクション型(List など)に関する知識があること。

学習内容

  • ラムダ式を文字列に埋め込む方法。
  • List コレクションに対してさまざまな高階関数(forEach()map()filter()groupBy()fold()sortedBy() など)を使用する方法。

必要なもの

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

2. forEach() とラムダを含む文字列テンプレート

スターター コード

以下の例では、洋菓子店の美味しいクッキーのメニューを表す List を例として取り上げ、高階関数を使用してさまざまな方法でメニューの書式を設定します。

最初に、初期コードをセットアップします。

  1. Kotlin のプレイグラウンドに移動します。
  2. main() 関数の上に Cookie クラスを追加します。Cookie の各インスタンスはメニューのアイテムを表し、nameprice、およびクッキーに関するその他の情報を含みます。
class Cookie(
    val name: String,
    val softBaked: Boolean,
    val hasFilling: Boolean,
    val price: Double
)

fun main() {

}
  1. 次に示すように、Cookie クラスの下、main() の外側に、クッキーのリストを作成します。型は List<Cookie> であると推定されます。
class Cookie(
    val name: String,
    val softBaked: Boolean,
    val hasFilling: Boolean,
    val price: Double
)

val cookies = listOf(
    Cookie(
        name = "Chocolate Chip",
        softBaked = false,
        hasFilling = false,
        price = 1.69
    ),
    Cookie(
        name = "Banana Walnut",
        softBaked = true,
        hasFilling = false,
        price = 1.49
    ),
    Cookie(
        name = "Vanilla Creme",
        softBaked = false,
        hasFilling = true,
        price = 1.59
    ),
    Cookie(
        name = "Chocolate Peanut Butter",
        softBaked = false,
        hasFilling = true,
        price = 1.49
    ),
    Cookie(
        name = "Snickerdoodle",
        softBaked = true,
        hasFilling = false,
        price = 1.39
    ),
    Cookie(
        name = "Blueberry Tart",
        softBaked = true,
        hasFilling = true,
        price = 1.79
    ),
    Cookie(
        name = "Sugar and Sprinkles",
        softBaked = false,
        hasFilling = false,
        price = 1.39
    )
)

fun main() {

}

forEach() でリストをループする

最初に学ぶ高階関数は、forEach() 関数です。forEach() 関数は、コレクション内の各アイテムに対して、パラメータとして渡された関数を一度だけ実行します。この関数は、repeat() 関数または for ループと同じように機能します。ラムダは、コレクション内の最初の要素から最後の要素まで、各要素に対して順番に実行されます。メソッド シグネチャは次のとおりです。

forEach(action: (T) -> Unit)

forEach() は、単一のアクション パラメータ(つまり型が (T) -> Unit の関数)を受け取ります。

T は、コレクションに含まれる任意のデータ型に対応します。ラムダは単一のパラメータを受け取るので、名前を省略して it でパラメータを参照できます。

forEach() 関数を使用して、cookies リストのアイテムを出力します。

  1. main() で、後置ラムダ構文を使用して cookies リストから forEach() を呼び出します。後置ラムダが唯一の引数であるため、関数を呼び出すときにかっこを省略できます。
fun main() {
    cookies.forEach {

    }
}
  1. ラムダの本文に、it を出力する println() ステートメントを追加します。
fun main() {
    cookies.forEach {
        println("Menu item: $it")
    }
}
  1. コードを実行し、出力を確認します。型の名前(Cookie)とオブジェクトの一意の識別子のみが出力され、オブジェクトの内容は出力されません。
Menu item: Cookie@5a10411
Menu item: Cookie@68de145
Menu item: Cookie@27fa135a
Menu item: Cookie@46f7f36a
Menu item: Cookie@421faab1
Menu item: Cookie@2b71fc7e
Menu item: Cookie@5ce65a89

文字列に式を埋め込む

文字列テンプレートを初めて紹介したとき、変数名にドル記号($)を付けて文字列に挿入する手法について説明しました。しかし、この手法は、プロパティにアクセスするためにドット演算子(.)と組み合わせると、思ったようには機能しません。

  1. forEach() の呼び出しで、ラムダの本文を変更して $it.name を文字列に挿入します。
cookies.forEach {
    println("Menu item: $it.name")
}
  1. コードを実行します。クラス名 Cookie と、オブジェクトの一意の識別子が挿入され、その後に .name が続きます。name プロパティの値はアクセスされません。
Menu item: Cookie@5a10411.name
Menu item: Cookie@68de145.name
Menu item: Cookie@27fa135a.name
Menu item: Cookie@46f7f36a.name
Menu item: Cookie@421faab1.name
Menu item: Cookie@2b71fc7e.name
Menu item: Cookie@5ce65a89.name

プロパティにアクセスしてそれらを文字列に埋め込むには、式が必要です。文字列テンプレートに式を含めるには、中かっこで囲みます。

2c008744cee548cc.png

ラムダ式は、左中かっこと右中かっこの間に配置します。ラムダ式でプロパティへのアクセス、算術演算の実行、関数の呼び出しなどを行うことができ、ラムダの戻り値が文字列に挿入されます。

コードを変更して、名前が文字列に挿入されるようにしましょう。

  1. it.name を中かっこで囲んでラムダ式にします。
cookies.forEach {
    println("Menu item: ${it.name}")
}
  1. コードを実行します。出力に、各 Cookiename が示されます。
Menu item: Chocolate Chip
Menu item: Banana Walnut
Menu item: Vanilla Creme
Menu item: Chocolate Peanut Butter
Menu item: Snickerdoodle
Menu item: Blueberry Tart
Menu item: Sugar and Sprinkles

3. map()

map() 関数を使用すると、あるコレクションを同じ数の要素を持つ新しいコレクションに変換できます。たとえば、map() を使用して、List<Cookie> をクッキーの name のみを含む List<String> に変換できます。そのためには、Cookie の各アイテムから String を作成する方法を map() 関数に指示します。

e0605b7b09f91717.png

洋菓子店のインタラクティブなメニューを表示するアプリを作成する例で考えてみましょう。ユーザーは、クッキー メニューの画面に移動したときに、名前の次に価格が表示されるなどの合理的な順序でデータが表示されることを望むはずです。map() 関数を使用すると、関連するデータ(名前と価格)が書式設定された文字列のリストを作成できます。

  1. main() から、既存のコードをすべて削除します。fullMenu という名前の新しい変数を作成し、cookies リストで map() を呼び出した結果を割り当てます。
val fullMenu = cookies.map {

}
  1. ラムダの本文に、itnameprice を含む書式設定された文字列を追加します。
val fullMenu = cookies.map {
    "${it.name} - $${it.price}"
}
  1. fullMenu の内容を出力します。これは forEach() で行えます。map() から返される fullMenu コレクションの型は、List<Cookie> ではなく List<String> です。cookies 内の各 Cookie は、fullMenu 内の String に対応します。
println("Full menu:")
fullMenu.forEach {
    println(it)
}
  1. コードを実行します。出力は fullMenu リストの内容と一致します。
Full menu:
Chocolate Chip - $1.69
Banana Walnut - $1.49
Vanilla Creme - $1.59
Chocolate Peanut Butter - $1.49
Snickerdoodle - $1.39
Blueberry Tart - $1.79
Sugar and Sprinkles - $1.39

4. filter()

filter() 関数を使用すると、コレクションのサブセットを作成できます。たとえば、数値のリストがある場合は、filter() を使用して、2 で割り切れる数値のみを含む新しいリストを作成できます。

d4fd6be7bef37ab3.png

map() 関数の結果は常に元のコレクションと同じサイズのコレクションになりますが、filter() 関数の結果は元のコレクション以下のサイズのコレクションになります。map() と異なり、結果のコレクションのデータ型は元のコレクションと同じになるので、List<Cookie> をフィルタするともう一つの List<Cookie> になります。

map() および forEach() と同様に、filter() は単一のラムダ式をパラメータとして受け取ります。ラムダは、コレクション内の各アイテムを表す単一のパラメータを受け取り、Boolean 値を返します。

コレクション内の各アイテムについて、次の処理が行われます。

  • ラムダ式の結果が true であれば、アイテムは新しいコレクションに含められます。
  • ラムダ式の結果が false であれば、アイテムは新しいコレクションに含められません。

これは、アプリ内でデータのサブセットを取得したい場合に便利です。たとえば、洋菓子店がメニューの中にソフトクッキーを強調表示した特別な欄を作りたいと考えたとします。この場合、アイテムを出力する前に、cookies リストを filter() できます。

  1. main() 内で、softBakedMenu という名前の新しい変数を作成し、cookies リストに対する filter() の呼び出しの結果を割り当てます。
val softBakedMenu = cookies.filter {
}
  1. ラムダの本文に、クッキーの softBaked プロパティが true と等しいかどうかをチェックするブール式を追加します。softBaked 自体が Boolean であるため、ラムダ本文には it.softBaked のみを含めれば済みます。
val softBakedMenu = cookies.filter {
    it.softBaked
}
  1. forEach() を使用して、softBakedMenu の内容を出力します。
println("Soft cookies:")
softBakedMenu.forEach {
    println("${it.name} - $${it.price}")
}
  1. コードを実行します。前と同様にメニューが出力されますが、今度はソフトクッキーだけが含まれます。
...
Soft cookies:
Banana Walnut - $1.49
Snickerdoodle - $1.39
Blueberry Tart - $1.79

5. groupBy()

groupBy() 関数を使用すると、関数に基づいてリストをマップに変換できます。関数の一意の戻り値は、それぞれが結果のマップのキーになります。各キーに対応する値は、その一意の戻り値を生成したコレクション内のすべてのアイテムです。

54e190b34d9921c0.png

キーのデータ型は、groupBy() に渡された関数の戻り値の型と同じです。値のデータ型は、元のリストのアイテムのリストです。

概念化が難しいため、単純な例から始めましょう。前と同じ数値リストを奇数と偶数でグループ化するとします。

数値が奇数か偶数かは、2 で割った余りが 01 かをチェックすることで判定できます。余りが 0 であれば、数値は偶数です。余りが 1 であれば、数値は奇数です。

この計算は剰余演算子(%)で行えます。剰余演算子は、式の左辺の被除数を式の右辺の除数で除算します。

4c3333da9e5ee352.png

剰余演算子は、除算演算子(/)のように除算の結果を返すのではなく、余りを返します。これは、数値が偶数か奇数かを判定する場合に便利です。

4219eacdaca33f1d.png

ラムダ式 { it % 2 } を使用して、groupBy() 関数を呼び出します。

結果のマップには、01 の 2 つのキーがあります。各キーは List<Int> 型の値を持ちます。キー 0 のリストにはすべての偶数の数値が含まれ、キー 1 のリストにはすべての奇数の数値が含まれます。

現実のユースケースとしては、写真の被写体や撮影場所で写真をグループ化する写真アプリがあります。洋菓子店のメニューの例では、クッキーがソフトクッキーかどうかでグループ化します。

groupBy() を使用し、softBaked プロパティに基づいてメニューをグループ化します。

  1. 前のステップのコードから filter() の呼び出しを削除します。

削除するコード

val softBakedMenu = cookies.filter {
    it.softBaked
}
println("Soft cookies:")
softBakedMenu.forEach {
    println("${it.name} - $${it.price}")
}
  1. cookies リストで groupBy() を呼び出し、結果を groupedMenu という名前の変数に格納します。
val groupedMenu = cookies.groupBy {}
  1. it.softBaked を返すラムダ式を渡します。戻り値の型は Map<Boolean, List<Cookie>> になります。
val groupedMenu = cookies.groupBy { it.softBaked }
  1. groupedMenu[true] の値を含む softBakedMenu 変数と、groupedMenu[false] の値を含む crunchyMenu 変数を作成します。Map の添字指定の結果は null 値許容であるため、Elvis 演算子(?:)を使用して空のリストを返すことができます。
val softBakedMenu = groupedMenu[true] ?: listOf()
val crunchyMenu = groupedMenu[false] ?: listOf()
  1. ソフトクッキーのメニューを出力し、次にクランチ クッキーのメニューを出力するコードを追加します。
println("Soft cookies:")
softBakedMenu.forEach {
    println("${it.name} - $${it.price}")
}
println("Crunchy cookies:")
crunchyMenu.forEach {
    println("${it.name} - $${it.price}")
}
  1. コードを実行します。groupBy() 関数により、いずれかのプロパティの値に基づいてリストが 2 つに分割されます。
...
Soft cookies:
Banana Walnut - $1.49
Snickerdoodle - $1.39
Blueberry Tart - $1.79
Crunchy cookies:
Chocolate Chip - $1.69
Vanilla Creme - $1.59
Chocolate Peanut Butter - $1.49
Sugar and Sprinkles - $1.39

6. fold()

fold() 関数は、コレクションから単一の値を生成するために使用します。合計価格を計算する場合や、リスト内のすべての要素を合計して平均値を求める場合などによく使用されます。

a9e11a1aad05cb2f.png

fold() 関数は、次の 2 つのパラメータを受け取ります。

  • 初期値。データ型は関数の呼び出し時に推定されます(つまり、0 の初期値は Int であると推定されます)。
  • 初期値と同じ型の値を返すラムダ式。

ラムダ式は、さらに次の 2 つのパラメータを受け取ります。

  • 1 つ目のパラメータはアキュムレータと呼ばれます。データ型は初期値と同じです。これは累積合計と見なされます。アキュムレータは、ラムダ式が呼び出されるたびに、前回ラムダが呼び出された時点の戻り値と等しくなります。
  • 2 つ目のパラメータは、コレクション内の各要素と同じ型です。

これまで見てきた他の関数と同様に、ラムダ式はコレクション内の各要素に対して呼び出されるので、fold() はすべての要素を合計する簡単な方法として使用できます。

fold() を使用して、すべてのクッキーの合計価格を計算しましょう。

  1. main() 内で、totalPrice という名前の新しい変数を作成し、cookies リストに対する fold() の呼び出しの結果を割り当てます。初期値として 0.0 を渡します。その型は Double であると推定されます。
val totalPrice = cookies.fold(0.0) {
}
  1. ラムダ式のパラメータを両方とも指定する必要があります。アキュムレータには total を使用し、コレクション要素には cookie を使用します。パラメータ リストの後に矢印(->)を指定します。
val totalPrice = cookies.fold(0.0) {total, cookie ->
}
  1. ラムダの本文で、totalcookie.price の和を計算します。これは戻り値であると推定され、次にラムダが呼び出されたときに total に渡されます。
val totalPrice = cookies.fold(0.0) {total, cookie ->
    total + cookie.price
}
  1. 読みやすくするため、totalPrice の値を文字列として書式設定して出力します。
println("Total price: $${totalPrice}")
  1. コードを実行します。結果は、cookies リストの価格の合計と等しくなります。
...
Total price: $10.83

7. sortedBy()

コレクションについて初めて学習したとき、要素の並べ替えに sort() 関数を使用できることを学びました。しかし、この関数は Cookie オブジェクトのコレクションでは機能しません。Cookie クラスには複数のプロパティがありますが、どのプロパティ(nameprice など)を並べ替えようとしているかを Kotlin は認識しません。

このような場合のために、Kotlin コレクションには sortedBy() 関数が用意されています。sortedBy() を使用すると、並べ替え対象のプロパティを返すラムダを指定できます。たとえば、price で並べ替えたい場合、ラムダは it.price を返します。値のデータ型に自然な並べ替え順序(文字列の場合はアルファベット順、数値の場合は昇順)があれば、その型のコレクションと同様に並べ替えられます。

5fce4a067d372880.png

sortedBy() を使用して、クッキーのリストをアルファベット順に並べ替えます。

  1. main() 内で、既存のコードの後に alphabeticalMenu という名前の新しい変数を追加し、cookies リストに対する sortedBy() の呼び出しを割り当てます。
val alphabeticalMenu = cookies.sortedBy {
}
  1. ラムダ式内で it.name を返します。結果のリストは、型はまだ List<Cookie> のままですが、name に基づいて並べ替えられます。
val alphabeticalMenu = cookies.sortedBy {
    it.name
}
  1. クッキーの名前を alphabeticalMenu に出力します。forEach() を使用すると、それぞれの名前を新しい行に出力できます。
println("Alphabetical menu:")
alphabeticalMenu.forEach {
    println(it.name)
}
  1. コードを実行します。クッキーの名前がアルファベット順に出力されます。
...
Alphabetical menu:
Banana Walnut
Blueberry Tart
Chocolate Chip
Chocolate Peanut Butter
Snickerdoodle
Sugar and Sprinkles
Vanilla Creme

8. おわりに

お疲れさまでした。コレクションに対して高階関数を使用できる例をいくつか学びました。並べ替えやフィルタリングなどの一般的な操作を 1 行のコードで実行できるので、プログラムがより簡潔でわかりやくなります。

まとめ

  • forEach() を使用すると、コレクション内の各要素をループできます。
  • 文字列に式を挿入できます。
  • map() を使用すると、コレクション(ほとんどの場合はデータ型が異なるコレクション)内のアイテムを書式設定できます。
  • filter() を使用すると、コレクションのサブセットを生成できます。
  • groupBy() は、関数の戻り値に基づいてコレクションを分割します。
  • fold() は、コレクションを単一の値に変換します。
  • sortedBy() を使用すると、指定したプロパティでコレクションを並べ替えることができます。

9. 関連リンク