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
を例として取り上げ、高階関数を使用してさまざまな方法でメニューの書式を設定します。
最初に、初期コードをセットアップします。
- Kotlin のプレイグラウンドに移動します。
main()
関数の上にCookie
クラスを追加します。Cookie
の各インスタンスはメニューのアイテムを表し、name
、price
、およびクッキーに関するその他の情報を含みます。
class Cookie(
val name: String,
val softBaked: Boolean,
val hasFilling: Boolean,
val price: Double
)
fun main() {
}
- 次に示すように、
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
リストのアイテムを出力します。
main()
で、後置ラムダ構文を使用してcookies
リストからforEach()
を呼び出します。後置ラムダが唯一の引数であるため、関数を呼び出すときにかっこを省略できます。
fun main() {
cookies.forEach {
}
}
- ラムダの本文に、
it
を出力するprintln()
ステートメントを追加します。
fun main() {
cookies.forEach {
println("Menu item: $it")
}
}
- コードを実行し、出力を確認します。型の名前(
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
文字列に式を埋め込む
文字列テンプレートを初めて紹介したとき、変数名にドル記号($
)を付けて文字列に挿入する手法について説明しました。しかし、この手法は、プロパティにアクセスするためにドット演算子(.
)と組み合わせると、思ったようには機能しません。
forEach()
の呼び出しで、ラムダの本文を変更して$it.name
を文字列に挿入します。
cookies.forEach {
println("Menu item: $it.name")
}
- コードを実行します。クラス名
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
プロパティにアクセスしてそれらを文字列に埋め込むには、式が必要です。文字列テンプレートに式を含めるには、中かっこで囲みます。
ラムダ式は、左中かっこと右中かっこの間に配置します。ラムダ式でプロパティへのアクセス、算術演算の実行、関数の呼び出しなどを行うことができ、ラムダの戻り値が文字列に挿入されます。
コードを変更して、名前が文字列に挿入されるようにしましょう。
it.name
を中かっこで囲んでラムダ式にします。
cookies.forEach {
println("Menu item: ${it.name}")
}
- コードを実行します。出力に、各
Cookie
のname
が示されます。
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()
関数に指示します。
洋菓子店のインタラクティブなメニューを表示するアプリを作成する例で考えてみましょう。ユーザーは、クッキー メニューの画面に移動したときに、名前の次に価格が表示されるなどの合理的な順序でデータが表示されることを望むはずです。map()
関数を使用すると、関連するデータ(名前と価格)が書式設定された文字列のリストを作成できます。
main()
から、既存のコードをすべて削除します。fullMenu
という名前の新しい変数を作成し、cookies
リストでmap()
を呼び出した結果を割り当てます。
val fullMenu = cookies.map {
}
- ラムダの本文に、
it
のname
とprice
を含む書式設定された文字列を追加します。
val fullMenu = cookies.map {
"${it.name} - $${it.price}"
}
fullMenu
の内容を出力します。これはforEach()
で行えます。map()
から返されるfullMenu
コレクションの型は、List<Cookie>
ではなくList<String>
です。cookies
内の各Cookie
は、fullMenu
内のString
に対応します。
println("Full menu:")
fullMenu.forEach {
println(it)
}
- コードを実行します。出力は
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 で割り切れる数値のみを含む新しいリストを作成できます。
map()
関数の結果は常に元のコレクションと同じサイズのコレクションになりますが、filter()
関数の結果は元のコレクション以下のサイズのコレクションになります。map()
と異なり、結果のコレクションのデータ型は元のコレクションと同じになるので、List<Cookie>
をフィルタするともう一つの List<Cookie>
になります。
map()
および forEach()
と同様に、filter()
は単一のラムダ式をパラメータとして受け取ります。ラムダは、コレクション内の各アイテムを表す単一のパラメータを受け取り、Boolean
値を返します。
コレクション内の各アイテムについて、次の処理が行われます。
- ラムダ式の結果が
true
であれば、アイテムは新しいコレクションに含められます。 - ラムダ式の結果が
false
であれば、アイテムは新しいコレクションに含められません。
これは、アプリ内でデータのサブセットを取得したい場合に便利です。たとえば、洋菓子店がメニューの中にソフトクッキーを強調表示した特別な欄を作りたいと考えたとします。この場合、アイテムを出力する前に、cookies
リストを filter()
できます。
main()
内で、softBakedMenu
という名前の新しい変数を作成し、cookies
リストに対するfilter()
の呼び出しの結果を割り当てます。
val softBakedMenu = cookies.filter {
}
- ラムダの本文に、クッキーの
softBaked
プロパティがtrue
と等しいかどうかをチェックするブール式を追加します。softBaked
自体がBoolean
であるため、ラムダ本文にはit.softBaked
のみを含めれば済みます。
val softBakedMenu = cookies.filter {
it.softBaked
}
forEach()
を使用して、softBakedMenu
の内容を出力します。
println("Soft cookies:")
softBakedMenu.forEach {
println("${it.name} - $${it.price}")
}
- コードを実行します。前と同様にメニューが出力されますが、今度はソフトクッキーだけが含まれます。
... Soft cookies: Banana Walnut - $1.49 Snickerdoodle - $1.39 Blueberry Tart - $1.79
5. groupBy()
groupBy()
関数を使用すると、関数に基づいてリストをマップに変換できます。関数の一意の戻り値は、それぞれが結果のマップのキーになります。各キーに対応する値は、その一意の戻り値を生成したコレクション内のすべてのアイテムです。
キーのデータ型は、groupBy()
に渡された関数の戻り値の型と同じです。値のデータ型は、元のリストのアイテムのリストです。
概念化が難しいため、単純な例から始めましょう。前と同じ数値リストを奇数と偶数でグループ化するとします。
数値が奇数か偶数かは、2
で割った余りが 0
か 1
かをチェックすることで判定できます。余りが 0
であれば、数値は偶数です。余りが 1
であれば、数値は奇数です。
この計算は剰余演算子(%
)で行えます。剰余演算子は、式の左辺の被除数を式の右辺の除数で除算します。
剰余演算子は、除算演算子(/
)のように除算の結果を返すのではなく、余りを返します。これは、数値が偶数か奇数かを判定する場合に便利です。
ラムダ式 { it % 2 }
を使用して、groupBy()
関数を呼び出します。
結果のマップには、0
と 1
の 2 つのキーがあります。各キーは List<Int>
型の値を持ちます。キー 0
のリストにはすべての偶数の数値が含まれ、キー 1
のリストにはすべての奇数の数値が含まれます。
現実のユースケースとしては、写真の被写体や撮影場所で写真をグループ化する写真アプリがあります。洋菓子店のメニューの例では、クッキーがソフトクッキーかどうかでグループ化します。
groupBy()
を使用し、softBaked
プロパティに基づいてメニューをグループ化します。
- 前のステップのコードから
filter()
の呼び出しを削除します。
削除するコード
val softBakedMenu = cookies.filter {
it.softBaked
}
println("Soft cookies:")
softBakedMenu.forEach {
println("${it.name} - $${it.price}")
}
cookies
リストでgroupBy()
を呼び出し、結果をgroupedMenu
という名前の変数に格納します。
val groupedMenu = cookies.groupBy {}
it.softBaked
を返すラムダ式を渡します。戻り値の型はMap<Boolean, List<Cookie>>
になります。
val groupedMenu = cookies.groupBy { it.softBaked }
groupedMenu[true]
の値を含むsoftBakedMenu
変数と、groupedMenu[false]
の値を含むcrunchyMenu
変数を作成します。Map
の添字指定の結果は null 値許容であるため、Elvis 演算子(?:
)を使用して空のリストを返すことができます。
val softBakedMenu = groupedMenu[true] ?: listOf()
val crunchyMenu = groupedMenu[false] ?: listOf()
- ソフトクッキーのメニューを出力し、次にクランチ クッキーのメニューを出力するコードを追加します。
println("Soft cookies:")
softBakedMenu.forEach {
println("${it.name} - $${it.price}")
}
println("Crunchy cookies:")
crunchyMenu.forEach {
println("${it.name} - $${it.price}")
}
- コードを実行します。
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()
関数は、コレクションから単一の値を生成するために使用します。合計価格を計算する場合や、リスト内のすべての要素を合計して平均値を求める場合などによく使用されます。
fold()
関数は、次の 2 つのパラメータを受け取ります。
- 初期値。データ型は関数の呼び出し時に推定されます(つまり、
0
の初期値はInt
であると推定されます)。 - 初期値と同じ型の値を返すラムダ式。
ラムダ式は、さらに次の 2 つのパラメータを受け取ります。
- 1 つ目のパラメータはアキュムレータと呼ばれます。データ型は初期値と同じです。これは累積合計と見なされます。アキュムレータは、ラムダ式が呼び出されるたびに、前回ラムダが呼び出された時点の戻り値と等しくなります。
- 2 つ目のパラメータは、コレクション内の各要素と同じ型です。
これまで見てきた他の関数と同様に、ラムダ式はコレクション内の各要素に対して呼び出されるので、fold()
はすべての要素を合計する簡単な方法として使用できます。
fold()
を使用して、すべてのクッキーの合計価格を計算しましょう。
main()
内で、totalPrice
という名前の新しい変数を作成し、cookies
リストに対するfold()
の呼び出しの結果を割り当てます。初期値として0.0
を渡します。その型はDouble
であると推定されます。
val totalPrice = cookies.fold(0.0) {
}
- ラムダ式のパラメータを両方とも指定する必要があります。アキュムレータには
total
を使用し、コレクション要素にはcookie
を使用します。パラメータ リストの後に矢印(->
)を指定します。
val totalPrice = cookies.fold(0.0) {total, cookie ->
}
- ラムダの本文で、
total
とcookie.price
の和を計算します。これは戻り値であると推定され、次にラムダが呼び出されたときにtotal
に渡されます。
val totalPrice = cookies.fold(0.0) {total, cookie ->
total + cookie.price
}
- 読みやすくするため、
totalPrice
の値を文字列として書式設定して出力します。
println("Total price: $${totalPrice}")
- コードを実行します。結果は、
cookies
リストの価格の合計と等しくなります。
... Total price: $10.83
7. sortedBy()
コレクションについて初めて学習したとき、要素の並べ替えに sort()
関数を使用できることを学びました。しかし、この関数は Cookie
オブジェクトのコレクションでは機能しません。Cookie
クラスには複数のプロパティがありますが、どのプロパティ(name
、price
など)を並べ替えようとしているかを Kotlin は認識しません。
このような場合のために、Kotlin コレクションには sortedBy()
関数が用意されています。sortedBy()
を使用すると、並べ替え対象のプロパティを返すラムダを指定できます。たとえば、price
で並べ替えたい場合、ラムダは it.price
を返します。値のデータ型に自然な並べ替え順序(文字列の場合はアルファベット順、数値の場合は昇順)があれば、その型のコレクションと同様に並べ替えられます。
sortedBy()
を使用して、クッキーのリストをアルファベット順に並べ替えます。
main()
内で、既存のコードの後にalphabeticalMenu
という名前の新しい変数を追加し、cookies
リストに対するsortedBy()
の呼び出しを割り当てます。
val alphabeticalMenu = cookies.sortedBy {
}
- ラムダ式内で
it.name
を返します。結果のリストは、型はまだList<Cookie>
のままですが、name
に基づいて並べ替えられます。
val alphabeticalMenu = cookies.sortedBy {
it.name
}
- クッキーの名前を
alphabeticalMenu
に出力します。forEach()
を使用すると、それぞれの名前を新しい行に出力できます。
println("Alphabetical menu:")
alphabeticalMenu.forEach {
println(it.name)
}
- コードを実行します。クッキーの名前がアルファベット順に出力されます。
... 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()
を使用すると、指定したプロパティでコレクションを並べ替えることができます。