ラムダ

ラムダの書き方や注意点を学びます。

はじめに

ラムダは、リストの操作等でよく使用されることが多いため、リストに対しての操作を例にとって解説していきます。以下は、リストに格納するPersonオブジェクトです。
名前、年齢、性別を表すプロパティを持つ、ごく単純なデータクラスとなっています。

data class Person(val name: String, val age: Int, val gender: Gender) {
}

enum class Gender {
    MAN,
    WOMAN,
}

リストは以下のようなものを使用します。
特に断りのない場合は以下のリストを操作します。

val people = listOf(
            Person("Bob", 20, Gender.MAN),
            Person("Alice", 19, Gender.WOMAN),
            Person("John", 21, Gender.MAN),
            Person("Ema", 30, Gender.WOMAN),
            Person("Brad", 18, Gender.MAN))

ラムダとは

平たく言ってしまうと、他の関数に渡すことのできる関数のことです。

基本の書き方

ラムダ式は、{}で囲むのが基本の書き方です。 ただし、いろいろと省略記法があるので、順に見ていきましょう。
以下の例では、Personオブジェクトのリストから、年齢が最大であるPersonを取得します。

//一番冗長な書き方。引数にラムダを渡している。
people.maxBy({p: Person -> p.age})
//引数の最後がラムダ式の場合、()の外に出せる
people.maxBy() { p: Person -> p.age }
//さらに、引数がラムダのみの場合、()は省略できる
people.maxBy { p: Person -> p.age }
//型が推論されるため、Person型であることを明示しなくとも良い
people.maxBy { p -> p.age }
//さらに、ラムダで使用できる変数itを使用することで、変数宣言も不要となる
people.maxBy { it.age }

いろいろなコレクション操作APIを試す

ラムダの基本的な書き方などを理解したところで、実際にコレクションに対してラムダを利用していろいろな操作を行ってみましょう。
以下では、コードと、その実行結果を記載しています。

filter

フィルターを使用すると、ある条件を満たす要素のみを残す新たなリストを取得できます。
例として、成人している(年齢が20以上)の要素を抜き出してみます。

println(people.filter { it.age >= 20 })
[Person(name=Bob, age=20, gender=MAN), Person(name=John, age=21, gender=MAN), Person(name=Ema, age=30, gender=WOMAN)]

map

mapを使用するとコレクションの要素を別の型に変換したリストを取得できます。
例として、Personオブジェクトを名前(String)へ変換してみましょう。

println(people.map { it.name })
[Bob, Alice, John, Ema, Brad]

all

allを使用すると、コレクションの全てが指定された条件を満たすかを判定できます。 以下の例では、コレクションのPersonオブジェクトが全て30歳以上かを判定しています。

println(people.all { it.age >= 30 })
false

any

anyを使用すると、コレクション中にある条件を満たす要素が存在しているかどうかを判定できます。コレクション中に1件でも条件に合致すればtrueとなります。

println(people.any { it.age >= 30 })
true

count

countを使用すると、コレクション中の要素の中から、条件に合う要素の個数を取得できます。 以下の例では、女性のカウントをしています。

println(people.count {it.gender == Gender.WOMAN})
2

find

findを使用すると、コレクションの中から、与えられた条件に合致するオブジェクトを取得します。 条件に合致するオブジェクトがない場合は、nullが返却されます。

    println(people.find { it.name == "Bob" })
    println(people.find { it.name == "Michel" })
Person(name=Bob, age=20, gender=MAN)
null

groupBy

groupByを使用すると、コレクションをある条件でグループ化したMapを取得できます。 取得できるMapの型は、Map<指定したキー, リスト>となります。
よって、以下の例では、Map<Gender, List<Person>>が取得できます。

println(people.groupBy { it.gender })
{MAN=[Person(name=Bob, age=20, gender=MAN), Person(name=John, age=21, gender=MAN), Person(name=Brad, age=18, gender=MAN)], WOMAN=[Person(name=Alice, age=19, gender=WOMAN), Person(name=Ema, age=30, gender=WOMAN)]}

flatMap

リストのリストなどを平坦に(flatに)するさいに利用します。 実際の例を見てみるとわかりやすいでしょう。 以下は、90年代のNBAチームから抜粋したチームオブジェクトを使用しています。

//各チームには文字列リストでメンバーの名前を保持している
val bulls = Team("bulls", listOf("Michael Jordan", "Scottie Maurice Pippen", "Dennis Keith Rodman"))
val jazz = Team("jazz", listOf("Karl Malone", "John Stockton"))

val teams = listOf(bulls, jazz)

println(teams.flatMap { it.members })
[Michael Jordan, Scottie Maurice Pippen, Dennis Keith Rodman, Karl Malone, John Stockton]

withとapply

with

まずはwithから見ていきましょう。

fun alphabet(): String {
    val sb = StringBuilder()
    return with(sb) { //with(sb)とすることで、このブロック内ではsbがレシーバとなる。
        for (letter in 'A'..'Z') {
            append(letter) //appendは変数sbに対して実行される
        }
        append("¥nNow I know the alphabet!")
        toString() //最終的にtoString()した結果がreturnされる
    }
}

withを使用することで、ラムダ内で使用するオブジェクトを指定できます。 このようにすることで、ラムダないで変数名を何度も書くことなくラムダをすっきりと書くことができるようになります。

apply

applyも、withとほぼ同様の動きをします。withとの違いは、applyは常に引数の値として渡されたオブジェクトを返すことです。
先ほどと同じalphabet関数をapplyを使用してリファクタリングしてみましょう。

fun alphabet() = StringBuilder().apply {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("¥nNow I know the alphabet!")
}.toString()

applyを使用すると、レシーバオブジェクトを直接返却するようになります。 そのため、上記の例ではオブジェクトの返却の前にtoString()を呼び出しています。
toString()を呼び出さない場合はStringBuilderオブジェクトが返却されます。

終わりに

ラムダについて、その書き方や使い方などをみてきました。 コレクションAPIとの親和性が高いので、上手に使えるようになると可読性の向上など、メリットは大きそうですね。

今回は以上となります。それではまた。