Kotlinラムダ式(Lambda expression)

概要

ここでは、匿名関数を扱うラムダ式とクロージャについて見ていく。似ているようで異なる点があるため、ここで確認する。

匿名関数(Anonymous function)

まず、匿名関数(Anonymous function)とは何かを例を見ながら確認する。

あらかじめ定義された関数に::を使用すると、関数オブジェクトへ変換する一連の流れを経ずに、関数オブジェクトを直接生成できる。次の例の(1)、(2)、(3)は同じ結果を得られる。

例: 関数オブジェクトのリテラル表現

// (1)
fun succ(n: Int): Int = n + 1
map(list, ::succ)

// (2)
map(list, fun(n: Int): Int { return n + 1 })

// (3)
map(list, {n: Int -> n + 1})

(2)や(3)のように関数オブジェクトを直接生成する方法を関数リテラル(Function literal)という。匿名関数(Anonymous function)ともいう。

(3)のような関数リテラルの形式を一般化すると次のようになる。

{引数リスト -> 関数本体}

ラムダ式(Lambda expression)

ラムダ式(ラムダ表現: Lambda expression)は、単にラムダとも呼ばれ、匿名関数(Anonymous function)を意味する。匿名関数とは、関数名がない、または省略された関数をいう。通常、一度だけ使用され再利用されない関数を作るときに匿名関数として作成し、変数に代入して利用したり、コールバック関数に指定したりできる。そのため、関数を別途作成せず、コード内に匿名関数を作ることでコードの可読性を高められる。これは関数型プログラミングでよく使われるパターンである。

fun main() {
    var add1 = fun(a: Int, b: Int): Int = a + b

    println(add1(3, 5))
}

Output:

8

クロージャ(closure)

関数リテラルには波括弧が必要である点に注意しよう。また、文脈によっては一部の記述を省略できる。

例: 関数リテラル表記法

// 関数リテラル内で型推論が可能である。
val foo: (Int) -> Int = { n ->
    n + 1
}

// 引数が1つの場合は、暗黙的にitを使用できる。
val bar: (Int) -> Int = {
    it + 1
}

// 複数の文を持つ関数リテラル
val baz: (Int) -> Int = {
    var sum = 0
    for (e in 1..it) {
        sum += e
    }
    sum
}

// 高階関数に渡す特殊な方法
map(listOf(1, 2, 3)) {
    it + 1
}

Kotlinの関数リテラルはクロージャ(closure)と呼ばれる。つまり、引数として与えられた変数以外の変数を、コードを書いたときのスコープで解決できる。文章で説明するより簡単なサンプルコードを見る方が分かりやすいため、例を見てみよう。

次の例は、クロージャを返すcounter関数である。

fun counter(): () -> Int {
    var count = 0
    return {
        count++
    }
}

この関数は「Intを返す関数」を返す。「Intを返す関数」を関数Aと呼ぶことにする。関数Aは{ count++ }である。変数countは関数Aの外側で宣言されているが、関数Aが定義された場所から参照し更新できる。関数Aを呼び出すとcountの値を返し、countは1ずつ増加する。

counter関数の使用例は次のとおりである。呼び出すたびに1ずつ増加する。

val counter1 = counter()
println(counter1()) // => 0
println(counter1()) // => 1
println(counter1()) // => 2

val counter2 = counter()
println(counter2()) // => 0
println(counter2()) // => 1

counter()によって関数Aを取得している。関数A(ここでは変数counter1として宣言されている)が呼び出されるたびに、返される値が増加していることが分かる。

ラムダ式とクロージャの違い

では、ラムダ式とクロージャの違いは何か。まず、ラムダとクロージャはどちらも匿名の特定機能ブロックである。

違いとして、クロージャは外部変数を参照し、ラムダはパラメーターだけを参照する点が異なる。

Closure例: 外部のcountという変数を参照

val count = 1

fun increase(): Int {
    return count + 1
}

Lambda例: 外部からcountというパラメーターを受け取る

  fun increase(count: Int): Int {
      return count + 1
  }

クロージャは外部への依存があり、ラムダは外部への依存がないstaticメソッドに似ている。

匿名関数の生成

匿名関数の簡単な使用例は次のとおりである。匿名関数とは、次のように名前なしで定義される関数(fun(str: String))をいう。

fun main() {
    // 匿名関数を生成してgreetingに代入する。
    val greeting1 = fun() { println("Hello world!") }

    // 匿名関数を呼び出す。
    greeting1()
}

Output:

Hello world!

上の例では、greeting変数に匿名関数が代入され、greeting匿名関数変数を呼び出している。

これをラムダを使うと、より簡単に匿名関数を定義できる。次の例は、上の例をラムダで書き直したコードである。

fun main() {
    val greeting2: () -> Unit = { println("Hello world!") }

    greeting2()
}

Output:

Hello world!

引数を受け取り、値を返す匿名関数

次の例は、引数を受け取って値を返す匿名関数である。

fun main() {
    val greeting3 = { name: String, age: Int -> "Hello. My name is $name. I'm $age year old." }

    val result3 = greeting3("devkuma", 21)

    println(result3)
}

Output:

Hello. My name is devkuma. I'm 21 year old.

->の左側には引数名(nameage)とデータ型(StringInt)が明示されている。->の右側には戻り値("Hello My name...")が書かれている。 returnは暗黙的であり、書かない。

引数の型を省略できる匿名関数

次の例は、引数の型を省略した匿名関数である。

fun main() {
    val greeting4: (String, Int) -> String = { name, age -> "Hello. My name is $name. I'm $age year old." }

    val result4 = greeting4("devkuma", 21)

    println(result4)
}

Output:

Hello. My name is devkuma. I'm 21 year old.

上の例では、greeting4変数に(String, Int) -> String関数を型として定義しているため、匿名関数ではname, ageのように引数の型を省略できる。

引数名を省略できる匿名関数

匿名関数の引数が1つの場合、引数名を省略できる。

次の例は、name変数を1つ受け取る匿名関数である。

fun main() {
    val greeting5: (String) -> String = { name -> "Hello. My name is $name." }

    val result5 = greeting5("devkuma")

    println(result5)
}

Output:

Hello. My name is devkuma.

上のように明示的に1つだけの変数(name)である場合は、変数名を省略することもできる。省略すると、iteratorの短縮形であるitという名前でアクセスする必要がある。使用例は次のとおりである。

fun main() {
    val greeting6: (String) -> String = { "Hello. My name is $it." }

    val result6 = greeting6("devkuma")

    println(result6)
}

Output:

Hello. My name is devkuma.

上の例ではnameを省略し、nameの代わりにitを使用していることが分かる。

これらをすべて省略できた理由は、変数にすでに引数と戻り値の型が定義されているためである。コンパイラーはこの情報をすべて知っているため、省略してもビルドエラーは発生しない。

ライブラリで使われる匿名関数

Kotlinでは、多くのオブジェクトに関数を引数として受け取る関数がある。匿名関数を使うと、次のように短いコードで簡単に実装できる。

fun main() {
    val numbers = listOf(5, 1, 3, 2, 9, 6, 7, 8, 4)
    println(numbers)

    val sorted = numbers.sortedBy { it }
    println(sorted)

    val biggerThan5 = numbers.sortedBy { it }.filter { it > 5 }
    println(biggerThan5)
}

Output:

[5, 1, 3, 2, 9, 6, 7, 8, 4]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
[6, 7, 8, 9]

sortedBy { it }での匿名関数は{ it }である。sortedBy関数の定義を見ると、引数が(T) -> R?として定義されているが、numbersがすでにIntであることが分かっているため省略できる。

public inline fun <T, R : Comparable<R>> Iterable<T>.sortedBy(crossinline selector: (T) -> R?): List<T> {
    return sortedWith(compareBy(selector))
}

.filter { it > 5 }の内部も匿名関数である。

このように匿名関数を使用すると、重複を取り除けるためコードが簡潔になる利点がある。

SAM(Single Abstract Method)

抽象メソッドが1つだけあるものをSAM(Single Abstract Method)という。ラムダ式でSAMの匿名オブジェクトを作成し、引数として渡せるが、これは匿名関数と似ている。

次のコードはAndroidでよく使われる匿名オブジェクトパターンであり、setOnClickListenerは匿名クラスを引数として受け取る。

匿名クラスのコードで実質的な実装コードはdoSomethingだけだが、クラスを定義するために形式的な(boilerplate)コードを書かなければならない。

button.setOnClickListener(object : OnClickListener{
  override fun onClick(view: View){
    doSomething()
  }
})

ラムダを使うと、実装コードだけを書いて引数として渡せる。引数の型はすでに定義されているため、型に合うラムダ式だけを書けば、コンパイラーが自動で匿名オブジェクトを作ってくれる。

次のコードは、ラムダで匿名クラスの引数を生成する例である。

fun setOnClickListener(listener: (View) -> Unit)

上のような関数は次のように呼び出せる。

button.setOnClickListener({ view -> doSomething() })

ここではコンパイラーが引数の型をすでに知っているため、次のように引数を省略できる。

button.setOnClickListener() { doSomething() }

さらに、引数が匿名関数または匿名クラスであれば、次のように関数の括弧(())も省略できる。

button.setOnClickListener { doSomething() }

省略できる部分を取り除いたことで、コードがかなり簡潔になった。

まとめると、上で見たように匿名関数を引数として渡しているというより、関数の実装内容のように見えるため、可読性を高められる構文である。ただし、関数の構造によっては可読性を下げる場合もあるため、使用時には注意が必要である。

参考