ティーポットは珈琲を淹れられない

ソフトウェアエンジニアK5のブログ

Kotlin文法 - 関数とラムダ

2016年1月29日2020年1月14日

Kotlin 文法 - ネストされたクラス、Enum クラス、オブジェクト、委譲、委譲されたプロパティの続き。

Kotlin Referenceの Functions and Lambdas 章の大雑把日本語訳。適宜説明を変えたり端折ったり補足したりしている。

この記事は Qiita の記事 と同じものです。

関数

fun キーワードを使って宣言する。

fun double(x: Int): Int {
}

関数の利用

val result = double(2)

メンバ関数(メソッド)の場合は

Sample().foo() // Sampleクラスのインスタンスを作ってfooを呼び出す

接中辞(infix)記法

次の場合に接中辞記法を使うことができる。

  • メンバ関数か拡張関数
  • 引数が1つだけ
  • 関数に infix キーワードが付けられている
//infixを付けてIntに拡張関数を追加
infix fun Int.shl(x: Int): Int {
...
}

//接中辞記法を使って拡張を呼び出す
1 shl 2
// 上は下と同じ
1.shl(2)

パラメータ

関数のパラメータは Pascal 記法、つまり 名前: 型 を用いて定義する。パラメータはコンマで区切られている。全てのパラメータは型を明示しなければならない。

fun powerOf(number: Int, exponent: Int) {
...
}

デフォルト引数

関数の引数はデフォルト値を持てる。これによってオーバーロードの数を減らすことができる。

// offのデフォルト値は0, lenのデフォルト値はb.size()
fun read(b: Array<Byte>, off: Int = 0, len: Int = b.size()) {
...
}

名前付き引数

関数を呼び出すときに引数名を指定できる。これはデフォルト値を持つパラメータが沢山あるときに便利。

fun reformat(str: String,
             normalizeCase: Boolean = true,
             upperCaseFirstLetter: Boolean = true,
             divideByCamelHumps: Boolean = false,
             wordSeparator: Char = ' ') {
...
}

// デフォルト値を使って呼ぶ
reformat(str)

// デフォルト値を使わないで呼ぶ
reformat(str, true, true, false, '_')

// 引数名を指定して呼ぶ
reformat(str,
    normalizeCase = true,
    upperCaseFirstLetter = true,
    divideByCamelHumps = false,
    wordSeparator = '_'
  )

// wordSeparatorだけデフォルト値を使わずに呼ぶ
reformat(str, wordSeparator = '_')

ただし Java 関数を呼ぶときは名前付き引数は使えない。Java のバイトコードは引数名を保存していないので。

Unit を返す関数

関数が有用なものを返さないなら戻り値は Unit 型になる。Unit はただ1つの値 Unit を持つ型。この型は明示的に返す必要はない。

fun printHello(name: String?): Unit {
    if (name != null)
        println("Hello ${name}")
    else
        println("Hi there!")
    // `return Unit` または `return` と書くのはオプション。書かなくてもいい。
}

関数宣言で戻り値に Unit と書くのは省略できる。上の関数宣言はこう書いても一緒。

fun printHello(name: String?) {
    ...
}

式が1つの関数

関数の中身が式1つだけなら、波括弧は省略できる。

fun double(x: Int): Int = x * 2

戻り値の型が推論可能ならそれも省略できる。

fun double(x: Int) = x * 2

明示的な戻り値型

ブロックを持つ関数は型が Unit でない限り、戻り値型を明示する必要がある。なぜならブロックを持つ関数は複雑な制御フローを持つかもしれず、戻り値型は読み手にとって(ときにはコンパイラにさえも)明らかでないから。

可変長引数(Varargs)

関数のパラメータ(普通は最後の1つ)には vararg 修飾子が付けられているかもしれない。

fun <T> asList(vararg ts: T): List<T> {
  val result = ArrayList<T>()
  for (t in ts) // ts は Array である
    result.add(t)
  return result
}

こうすると関数に渡す引数の数を可変にできる。

  val list = asList(1, 2, 3)

関数の中では T 型の引数 ts は T の配列に見える。すなわち上の例で ts の型は Array<out T> である。

パラメータのうち1つだけが vararg を付けられる。もしそれが最後の引数でなかったら、後続の引数は名前付き引数を使って渡すか、または後続の引数が関数型なら括弧の外からラムダを渡す。

// varargが付いてるのが最後の引数でない場合
fun testMiddleVararg(vararg ts: Int, lastArg: String) {
  // ...
}

// varargより後の引数は名前付きで渡す必要がある
testMiddleVararg(1, 2, 3, lastArg = "test")

// varargが付いてるのが最後の引数でなく、残りの引数が関数型の場合
fun testVarargWithFunc(vararg ts: Int, proc: () -> Unit) {
  // ...
}

// 引数を渡す括弧の外でラムダを渡すなら、それが最後の引数だと分かる
testVarargWithFunc(1, 2, 3) {
  // 最後の引数に渡す処理
}

既に渡したいものを配列として持ってるなら、配列の前にアスタリスクを付けることで展開して渡せる。

val a = arrayOf(1, 2, 3)        // 可変長引数に渡したいものを配列にする
val list = asList(-1, 0, *a, 4) // *をつけて展開して渡す

関数のスコープ

Kotlin では関数はファイルのトップレベルで宣言できる。つまり Java, C#, Scala のように関数を入れるためのクラスを用意する必要はない。トップレベルの関数に加えて、(関数内で)ローカルに、メンバ関数として、拡張関数として宣言することもできる。

メンバ関数や拡張関数はもう説明しているからいいよね?ローカル関数だけ説明するよ。

ローカル関数

関数の中で関数を宣言できる。

fun dfs(graph: Graph) {
  // この関数はこのブロックの中でしか使わないから、ローカルとして宣言しとこ
  fun dfs(current: Vertex, visited: Set<Vertex>) {
    if (!visited.add(current)) return
    for (v in current.neighbors)
      dfs(v, visited)
  }

  dfs(graph.vertices[0], HashSet())
}

ローカル関数はその外側の変数にアクセスできる。なので上の例で visited はローカル変数にできる。

fun dfs(graph: Graph) {
  val visited = HashSet<Vertex>()
  fun dfs(current: Vertex) {
    // この関数の中で、外側にあるvisitedが見える。
    if (!visited.add(current)) return
    for (v in current.neighbors)
      dfs(v)
  }

  dfs(graph.vertices[0])
}

ローカル関数はラベルを指定することで外側の関数を抜けることもできる。

fun reachable(from: Vertex, to: Vertex): Boolean {
  val visited = HashSet<Vertex>()
  fun dfs(current: Vertex) {
    // ここのreturnは外側の関数(reachable)を抜ける
    if (current == to) return@reachable true

    // ここのreturnはこのローカル関数(dfs)を抜ける
    if (!visited.add(current)) return

    for (v in current.neighbors)
      dfs(v)
  }

  dfs(from)
  return false // もしdfs()が既にreturn trueで抜けていなかったら
}

末尾再帰

Kotlin は末尾再帰(tail recursion)として知られる関数型プログラミングのスタイルをサポートしている。これは通常はループで書くアルゴリズムを、スタックオーバーフローを起こすリスクなく再帰を使って書けるようにしてくれる。要求される形を満たした上で関数の頭に tailrec 修飾子を付けると、コンパイラが再帰を(高速で効率的な)ループに置き換えて最適化する。

tailrec fun findFixPoint(x: Double = 1.0): Double
        = if (x == Math.cos(x)) x else findFixPoint(Math.cos(x))

これは数学の定数であるコサインの不動点を計算するコード。単純に 1.0 から始めて Math.cos()を結果がもう変化しなくなるまで繰り返し呼び出す。つまり 0.7390851332151607 が導き出されるまで。コンパイラが吐き出すコードは伝統的なスタイルの以下と同等のものになる。

private fun findFixPoint(): Double {
    var x = 1.0
    while (true) {
        val y = Math.cos(x)
        if (x == y) return y
        x = y
    }
}

tailrec に適合させるために、関数は最後の処理として自分自身を呼び出さなければならない。再帰呼び出しの後にまだコードがある場合、末尾再帰は使えない。また try/catch/finally ブロックの中でも使えない。今の所、末尾再帰は JVM バックエンドでのみサポートされている。

高階関数とラムダ

高階関数

高階関数はパラメータとして関数を取ったり、関数を返したりする関数のこと。ロックオブジェクトを受け取って関数を実行する lock() が良い例。これはロックを獲得してから関数を実行して、ロックを解除する。

// body は () -> T の関数型で、引数なしで T 型の戻り値を返す関数を表している。
fun <T> lock(lock: Lock, body: () -> T): T {
  lock.lock()  // 処理を実行する前にロックする
  try {
    return body()  // 渡された関数を実行
  }
  finally {
    lock.unlock()  // ロックを解除する(実行に成功しようが失敗しようが)
  }
}

これを呼び出すには引数として別の関数を渡す。

// toBeSynchronized()関数を宣言
fun toBeSynchronized() = sharedResource.operation()

// このtoBeSynchronized()関数を渡したいのだが、それには::で参照を取得する
val result = lock(lock, ::toBeSynchronized)

もっと簡便な方法はラムダを渡すこと。ラムダの書式については後で説明する。

val result = lock(lock, { sharedResource.operation() })

最後の引数が関数なら、引数を並べる( )括弧の外に出すのが Kotlin の慣習。

lock (lock) {
  sharedResource.operation()
}

他の高階関数の例として map() がある。

// List内の各要素を、渡された関数を使って別のものに置き換えたListを返す
fun <T, R> List<T>.map(transform: (T) -> R): List<R> {
  val result = arrayListOf<R>()
  for (item in this)
    result.add(transform(item))
  return result
}

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

// intsの各要素がitとして渡されるので、それを2倍にした要素に置き換えたリストを作成する
val doubled = ints.map { it -> it * 2 }

ラムダに渡される引数が1つだけなら、その名前はデフォルトで it なので省略できる。

ints.map { it * 2 }

これらの慣習を使うと C#の LINQ スタイル1のコードが書ける。

// stringsの要素のうち、長さが5のものだけ取り出して、それを要素自体で比較して並び替えて、
// 中身を大文字に変換したものを生成する
strings.filter { it.length == 5 }.sortBy { it }.map { it.toUpperCase() }

ラムダ式と無名関数

ラムダ式または無名関数は「関数リテラル」である。つまり関数は宣言されていないけど即座に式として渡せる。次の例を考えてみよう。

max(strings, { a, b -> a.length() < b.length() })

関数 max は高階関数、つまり2つ目の引数として関数を取る。2つ目の引数として渡しているものは式でありそれ自身が関数 − つまり関数リテラルである。関数として等価なのは以下になる。

fun compare(a: String, b: String): Boolean = a.length() < b.length()

関数型

引数として他の関数をとるためには、その関数の型を指定しないといけない。例えば上で出てきた max は以下のように定義される。

fun <T> max(collection: Collection<T>, less: (T, T) -> Boolean): T? {
  var max: T? = null
  for (it in collection)
    if (max == null || less(max, it))  // ここでlessを呼び出している
      max = it
  return max
}

パラメータ less の型は (T, T) -> Boolean で、これは型が T の2つの引数を取り、戻り値が Boolean の関数を表す。4行目でこの less を型 T の2つの引数を渡して呼び出している。

ラムダ式の書式

ラムダ式の完全な書式は以下になる。

val sum = { x: Int, y: Int -> x + y }

ラムダ式は波括弧で括られ、波括弧の中にあるその引数の型はオプション。関数の中身は -> の後に続く。引数の型を外に出すと次のようになる。

val sum: (Int, Int) -> Int = { x, y -> x + y }

無名関数

ラムダ式の書式で1つ抜け落ちているのは戻り値の型。大抵は自動的に推論できるからいらないんだけど、もし明示的に指定する必要があれば 無名関数 が使える。

fun(x: Int, y: Int): Int = x + y

名前がない以外は普通の関数と見た目は一緒。中身は式でもブロックでもいい。

fun(x: Int, y: Int): Int {
  return x + y
}

パラメータや戻り値の型は文脈から推論できるなら省略できる。戻り値の型の省略に関しては普通の関数とルールは同じ。

ints.filter(fun(item) = item > 0)

ラムダ式と違って無名関数は引数を並べる ( ) 括弧の外には出せない。

もう一つの重要なラムダ式との違いは、内部で return したときの動作。これは以前も説明した。ラベルを付けずに return した場合、ラムダは外側の関数を抜けるが、無名関数はその無名関数自身を抜ける。

クロージャ

(ローカル関数やオブジェクト式と同様に)ラムダ式や無名関数はその外側で宣言された変数にアクセスできる。Java と違ってクロージャにキャプチャした変数を変更できる。

var sum = 0
ints.filter { it > 0 }.forEach {
  sum += it  // このラムダ式はブロックの外側にあるsumにアクセスして、しかも変更している
}
print(sum)

レシーバ付きの関数リテラル

Kotlin はレシーバ(そのメソッドを呼び出されるオブジェクト)を指定して関数リテラルを呼び出す方法を提供している。関数リテラルのボディの中で、そのオブジェクトのメソッドを呼び出すことができる。これは拡張関数に似ている2。この機能の最も重要な利用例は型安全な Goovy スタイルのビルダーである。

レシーバ付きの関数リテラルの型は次のようになる。

// レシーバはInt型で、引数はother: Int、戻り値はInt型
sum : Int.(other: Int) -> Int

これはまるでレシーバオブジェクトのメソッドであるかのように呼び出せる。

// 1をレシーバにして、レシーバ付き関数リテラル sum を呼び出す
1.sum(2)

無名関数の書式なら関数リテラルの型を直接指定できる。これはレシーバ付き関数型の変数を宣言して、後でそれを使う場合に便利。

// レシーバはInt型で、引数はother:Int、戻り値はInt型のレシーバ付き無名関数をsumに代入
val sum = fun Int.(other: Int): Int = this + other  // 自身と引数otherを足した値を返す

レシーバ付きラムダ式は文脈からレシーバの型が推論できる場合に使える3

class HTML {
    fun body() { ... }
}

// init引数はレシーバがHTML型で、引数なし、戻り値なし(Unit)
fun html(init: HTML.() -> Unit): HTML {
  val html = HTML()  // レシーバオブジェクトを作る
  html.init()        // ラムダにレシーバオブジェクトを渡す
  return html
}

// html関数にレシーバ付きラムダを渡す
html {       // レシーバ付きラムダ開始
    body()   // レシーバオブジェクトのメソッドを呼び出す。this.body()
}

インライン関数

高階関数を使うことは幾らかの実行時のペナルティがあることを暗示している。これらの関数はオブジェクトであり、関数ボディからアクセスできるように外部環境をキャプチャする。メモリ確保と仮想関数呼び出し4は実行時のオーバーヘッドとなる。

多くの場合この種のオーバーヘッドはラムダ式をインラインにすることで除去できる。上で挙げた lock() 関数はその良い例で、簡単に呼び出し側でインライン展開できる。次の場合を考えてみよう。

lock(l) { foo() }

lock() に渡す関数オブジェクトを生成してそれを呼び出すコードを生成する代わりに、コンパイラが次のコードを吐き出してくれたなぁ。

l.lock()
try {
  foo()
}
finally {
  l.unlock()
}

手始めに欲しいのはこんなのじゃない?

これをコンパイラにやらせるために、lock() 関数の前に inline 修飾子を付ける必要がある。

inline fun lock<T>(lock: Lock, body: () -> T): T {
  // ...
}

インラインは生成されるコードの肥大化を招く。しかし妥当なやり方(つまりデカい関数をインラインにしない)をすれば、パフォーマンス上の効果を生む5

noinline

インライン関数の引数にラムダを渡す時、引数に noinline 修飾子を付けることができる。

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
  // ...
}

インライン化できるラムダは関数の中で呼び出すか、インライン化できる引数として渡すことしかできない。noinline が付けられたものは何とでも好きなように操作することができる。つまりフィールドに格納したり、持ち回したり。

注:もしインライン関数がインライン可能な関数パラメータも持たず、具象化型パラメータ(後述)も持たないなら、コンパイラは警告を発する。それらの関数をインライン化することにメリットがあるとは思えないので(もしインライン化が必要だとはっきりしているなら、警告を抑制できる)。

ローカルでない return

前にも書いたけど、Kotlin ではラベルなしの return は(無名含む)関数を抜けるけど、ラムダを抜けるにはラベルが必要。

fun foo() {
  ordinaryFunction {
     return // ERROR: ここではfooを抜けるreturnは作れない
  }
}

でもラムダがインラインとして渡されているなら、return はインライン展開されるのでこう書いてもいい。

fun foo() {
  inlineFunction {  // このラムダがインラインなら
    // このreturnはこのラムダではなくfooを抜ける
    return // OK: ラムダはインライン展開されて、returnの意味もfoo内に直接書いたのと同じになる。
  }
}

このような return (ラムダ内にあって、でも外側の関数を抜ける)を ローカルでない return と呼んでいる。インライン関数を使うループの中でこんな感じに利用するのに向いてる。

fun hasZeros(ints: List<Int>): Boolean {
  ints.forEach {
    if (it == 0) return true // hasZerosを抜ける
  }
  return false
}

インライン関数は渡されたラムダを関数ボディ内から直接呼び出すだけでなく、他の実行文脈から呼び出すこともある。ローカルオブジェクトやネストされた関数のような。そういう場合、ローカルでないフロー制御は許可されない。それを示すために、ラムダ引数に crossinline 修飾子を付ける必要がある。

// crossinlineを付けたので、bodyに渡すラムダはローカルなフロー制御を使ってはいけない。
// この関数の呼び出し元でそのようなラムダを渡すとコンパイルエラーになる。
inline fun f(crossinline body: () -> Unit) {
    val f = object: Runnable {
        // 渡されたbodyは関数fのボディ内でなく、ローカルオブジェクトから呼び出される
        override fun run() = body()
    }
    // ...
}

breakcontinue はまだインラインのラムダ内で利用できないけど、将来サポートしようとは思ってる。

具象化型パラメータ

しばしばパラメータとして渡された型にアクセスする必要がある。

fun <T> TreeNode.findParentOfType(clazz: Class<T>): T? {
    var p = parent
    while (p != null && !clazz.isInstance(p)) {
        p = p.parent
    }
    @Suppress("UNCHECKED_CAST")
    return p as T
}

ここではリフレクションを使ってノードがある型かどうかをチェックしている。うまく動くけど、呼び出す側は綺麗に書けない。

myTree.findParentOfType(MyTreeNodeType::class.java)

実際にやりたいことは、単にこの関数に型を渡したいだけ。こんな風に。

myTree.findParentOfType<MyTreeNodeType>()

これをできるようにするため、インライン関数は 具象化型パラメータ をサポートする。なのでこんな感じに書ける。

inline fun <reified T> TreeNode.findParentOfType(): T? {
    var p = parent
    while (p != null && p !is T) {
        p = p?.parent
    }
    return p as T
}

型パラメータに refied 修飾子を付けると、関数内でまるで普通のクラスであるかのようにアクセスできるようになる。関数はインライン展開されるので、リフレクションを使わずとも、 !is とか as とかが使える6

多くの場合リフレクションは必要にならないけど、具象化型パラメータに対しても使うことはできる。

inline fun <reified T> membersOf() = T::class.members

fun main(s: Array<String>) {
  println(membersOf<StringBuilder>().joinToString("\n"))
}

inline を付けられていない)普通の関数は具象化型パラメータを持てない。実行時表現を持たない型(つまり具象化可能でない型7Nothing のような架空の型)は具象化型パラメータとしては使えない。

次の章へ

次はKotlin 文法 - 分解宣言、範囲、型チェックとキャストへ GO!


  1. これ LINQ スタイルっていうのかな?LINQ って SQL っぽく書くやつだと思ってたけど。まぁ関数名が SQL ぽいってだけで概念は一緒だけど。見た目はものすっごい Ruby っぽいって思う。

  2. 拡張関数もボディ内でレシーバオブジェクトのメソッドを呼び出せる

  3. この例は DSL(Domain Specific Language)がこんな感じで作れるよって言いたいみたいね。

  4. 実行時にメモリからアドレスを取得して関数を呼び出すこと。

  5. 「特にループ内でメガモーフィック(megamorphic)なコールサイトにあるときに」という記述があったが難しい話なので削除。ようするにインライン化されていないと呼び出しにコストがかかるので、ループ内で何度も呼び出されるような状況なら高速化のためにキャシュする。つまり2回目からはオーバーヘッドがかからない。でもそういうのが多すぎてもうキャッシュできないよ〜って状態にある場合のこと。

  6. reified 付けなくても as は使えてるけど・・・

  7. 型消去が行われる型。型消去は Java5 より前の型とのバイトコード互換性のために行われる処理。


© 2016-2020 K5