おはようエンジニア

まともなエンジニアになりたい

『Kotlinイン・アクション』第5章を読む (その1)

Kotlinイン・アクション第5章は「ラムダ式を使ったプログラミング」です。

ラムダ式

ラムダ式とメンバ参照

  • ラムダ式(またはラムダ)とは
    • ほかの関数に渡すことできるコードの断片
  • ラムダ式を使うとどう嬉しいか
    • 関数に「振る舞い」を直接渡すことができる
    • そのために匿名の内部クラスを作ったり、名前付きの関数を宣言する必要がなくなる
  • Kotlinのコレクションの多くはラムダ式を渡して簡潔に利用できるように設計されている

ラムダ式の構文

// ラムダ式を変数に格納して使う
>>> val sum = { x: Int, y: Int -> x + y }
>>> println(sum(1,2))
3
// maxByにラムダ式を渡す
people.maxBy({ p: Person -> p.age })
// ↓↓ 関数の最後の引数がラムダなら {} を外に出せる
people.maxBy() { p: Person -> p.age }
// ↓↓ ラムダが関数唯一の引数なら () を省略できる
people.maxBy { p: Person -> p.age }
// ↓↓ 型推論可能な場合は型を省略できる
people.maxBy { p -> p.age }
// ↓↓ ラムダの引数がただ1つなら引数名を it で置き換えられる
people.maxBy { it.age }
  • 注意点
    • 意味を理解しにくくなる場合は引数や型を明示的に書く
    • 関数の引数が複数ある関数の場合は分かりにくくなるので通常の構文を使ったほうが良い
    • ネストされたラムダの場合は it は使わず明示的に引数を定義したほうが良い

スコープ内の変数アクセス

  • ラムダでは関数のローカル変数や引数にアクセスできる(変更も可)
    • ラムダによるキャプチャと呼ぶ
    • キャプチャされた変数の参照はラムダとともに保持される

メンバ参照

  • メンバ参照によってすでに定義済みの関数を値に変換できる
// ラムダ式による表現
val getAge = { person: Person -> person.age }
// メンバ参照による表現 (より簡潔)
val getAge = Person::age
fun salute() = println("Salute!")
>>> run(::salute) // クラス名を省略する
data class Person(val name: String, val age: Int)

>>> val createPerson = ::Person  // コンストラクタを保持
>>> val p = createPerson("Alice", 29)  // 遅延実行できる

コレクション操作のための関数型API

filter

ラムダが true を返した要素のみ抽出する

>>> val list = listOf(1, 2, 3, 4)
>>> println(list.filter { it % 2 == 0 })
[2, 4]

map

各要素にラムダを適用して新しいコレクションに変換する

>>> val list = listOf(1, 2, 3, 4)
>>> println(list.map { it * it })
[1, 4, 9, 16]
  • メンバ参照を渡すこともできる
  • マップに対してはキーと値を扱う mapKeys と mapValues が用意されている (filterも同様)

all

全ての要素が述部を満たすかどうかを返す

>>> val canBeInClub27 = { p: Person -> p.age <= 27 }  // 述部
>>> val people = listOf(Person("Alice", 27), ("Bob", 31))
>>> println(people.all(canBeInClub27))
false
  • !all (not all) は後述する any で置き換えたほうが見やすい
    • 例えば「全てが3に等しいわけではない」は「少なくとも一つの要素が3ではない」のように、逆条件の any で考える

any

少なくとも一つの要素が述部を満たすかどうかを返す

>>> println(people.any(canBeInClub27))
true

count

述部を満たす要素の数を返す

>>> println(people.count(canBeInClub27))
1

find

述部を満たす要素を返す

>>> println(people.find(canBeInClub27))
Person(name=Alice, age=27)
  • 一つもなければ null を返す
    • firstOrNull は find と同意で、考えを明確に表現したい場合に使う

groupBy

  • 全ての要素をある特性にしたがって分類したマップを返す
    • キーは述部の結果、値は該当する要素
>>> val people = listOf(Person("Alice", 27), ("Bob", 31), ("Carol", 31))
>>> println(people.groupBy{ it.age })
{27=[Person(name=Alice, age=27)],
31=[Person(name=Bob, age=31), Person(name=Carol, age=31)]}

flatMap

各要素を map した結果を一つのリストに結合する

>>> val strings = listOf("abc", "def")
>>> println(strings.flatMap { it.toList() })
[a, b, c, d, e, f]

flatten

ネストしたリスト内の要素を一つのリストに結合する

>>> val deepArray = listOf(listOf(1), listOf(2, 3), listOf(4, 5, 6))
>>> println(deepArray.flatMap{ it.toList() })
[1, 2, 3, 4, 5, 6]

ラムダを使ったいろいろな種類のコレクション操作が登場しました。

ここに出てきた処理はコードを書くときに頻繁に出てくるので積極的に使っていこうと思います。