読者です 読者をやめる 読者になる 読者になる

Kotlin Advent Calendar 2012 (全部俺)

JavaプログラマのためのKotlin入門

10日目:関数(後)

f:id:ngsw_taro:20121209130628j:plain

10日目の今日は、昨日に引き続きKotlinにおける関数について紹介したいと思います。高階関数や関数リテラルなど、Javaプログラマには馴染みのあまりない関数の使い方が登場します。

関数リテラル

関数リテラルとは、リテラルという言葉が示す通り、宣言を伴わずそのままコードを記述した関数のことです。次の例では関数をリテラルとして記述し、その場で呼び出しています。

val result = {
  (a : Int, b : Int) : Int -> a * b
}(3, 4)
  
println(result) // 12

関数リテラルの書き方を一般化すると次のようになります。

{
    ( 引数リスト ) : 返値型 -> 関数本体
}

関数リテラル内では return は使用できません最後の式(や値)がその関数の返り値と見なされます。

引数がない場合は、引数リストから -> までを省略することができます。

{
    println("Hi")
}()

気付いている方もいらっしゃると思いますが、重要なのは関数リテラルは、リテラルであるが故に変数へ代入できるということです!次のコードは、引数として与えられた整数値を2倍にして返す関数リテラルを変数 twice へ代入しています。代入後に変数を介して関数を呼び出しています。

val twice : (Int) -> Int = {
  (n : Int) -> n * 2
}
  
println(twice(123)) // 246

変数 twice には型が宣言されています。関数リテラルの型は、

( 引数の型リスト ) -> 返値型

と記述します。もちろん型推論が働くので、この場合は型を省略することが可能です。型を明示して、かつ関数リテラルの引数が1つだけの場合、次のような書き方ができます。

val twice : (n : Int) -> Int = {
  it * 2
}

唯一の引数へ it という名前を介して参照することができます。この便利さは高階関数で生きてきます。

高階関数

関数リテラルが変数へ代入できるならば、関数リテラルを関数の引数として渡すこともできるのではないかと推測できます。その通りです!関数を引数として受け取ったり、返り値として返したりするような関数のことを高階関数と呼びます。

fun main(args : Array<String>) {
  sayHello(
    {
      println("$it, world!")
    }
  )
}
 
fun sayHello(f : (String) -> Unit) {
  f("Hello")
}

上記の例のような場合、正確には最後の引数に関数リテラルを取る関数を呼び出す場合は、次のように記述することができます。

fun main(args : Array<String>) {
  sayHello {
    println("$it, world!")
  }
}

ここまでの例では高階関数の良さがわからないと思います(文法にフォーカスして説明して来たので)。高階関数を使うと、関数を抽象化することができます。つまり、高階関数と引数から受け取る関数は別の問題に集中することにより、それぞれの関数を部品化、再利用化することが容易になるのです。おっと、説明が抽象的すぎてしまいましたね。例を示します。

fun IntRange.each(f : (Int) -> Unit) {
  for(i in this) {
    f(i)
  }
}
 
fun main(args : Array<String>) {
  (1..10).each {
    println(it)
  }
}

関数 each を整数値の範囲を表す IntRange の拡張関数として定義しています。この関数では、範囲内の数値を順にループして、個々の数値に関する具体的な処理は、引数 として受け取る関数 f へ転送しています。関数 each の呼び出し側で関数リテラルを引数に渡しているのがわかると思います。この関数リテラルは受け取った数値を表示することに責任を持っています。

重要なのは、繰り返しと表示、それぞれの問題を切り分けて考えることができるということです。また、関数リテラル高階関数の引数として渡す際の構文糖衣により、関数呼び出し + 関数リテラル定義が、あたかも組み込み構文であるかのように見えることにも注目してください。つまり、これらの機能のおかげで、Kotlinは柔軟かつ可読性に優れたコードを容易に書けるのです。

Kotlinの標準ライブラリには、高階関数が多く見られます。特にコレクションの操作では高階関数の恩恵を最大限享受できます。

中置呼び出し

ある条件下で、関数を中置演算子のように呼び出すことができます。このことを中置呼び出しと言います。次の2つの条件を満たす場合、中置呼び出しができます。

  • メンバ関数(または拡張関数)である
  • 取る引数が1つである

中置呼び出しにより、高階関数を呼び出す記述をより簡潔にできます。繰り返しになりますが、それは組み込み構文であるかのような見た目になります。上記の例で登場した関数 each を中置呼び出ししてみます。

1..10 each {
  println(it)
}

クロージャ

Kotlinではクロージャを扱えます。クロージャ自体を説明するのは難しいのですが、要はローカル関数や関数リテラルが、その外側で定義されているローカル変数の値を見たり書いたりすることができるということです。

fun main(args : Array<String>) {
  var count = 0
  
  fun countUp() {
    count++
  }
  
  countUp()
  countUp()
  
  println(count) // 2
}

これはJavaではできません。メソッド内で定義した匿名クラスのメソッドからは、その外側のローカル変数の値を変更することはできません。final宣言されている変数のみ参照することはできます。

インライン関数

関数にアノテーション inline を付けると、その関数はインライン関数となり、コンパイル時に呼び出し側でインライン展開されます。高階関数は呼び出しコストが大きいため、必要に応じてインライン関数として定義することを検討すべきでしょう。ただし、現時点ではインライン関数は実装されていません。

inline fun doSomething() {
  // do something
}

関数と関数リテラル

キーワード fun により定義する関数と、関数リテラル別物です。関数リテラルを変数に代入し、引数として渡すという次のコードはうまく行きます。

fun sayHello(f : (String) -> Unit) {
  f("Hello")
}
 
fun main(args : Array<String>) {
  val writeStandardOutput = {
    (str : String) -> println(str)
  }
  
  sayHello(writeStandardOutput)
}

一方、キーワード fun によって定義された関数を使用した次のコードは残念なことにコンパイルエラーとなってしまいます。

fun sayHello(f : (String) -> Unit) {
  f("Hello")
}
 
inline fun writeStandardOutput(str : String) {
  println(str)
}
 
fun main(args : Array<String>) {
  sayHello(writeStandardOutput) // ERROR!!!
}

このことを考えると、Kotlinは関数型プログラミングを意識しつつも、非常に重視しているわけでもないことが伺えます。あくまで関数リテラルは記述量の低減や可読性の向上が目的のようです。

演算子オーバロード

2日目に少し紹介した演算子オーバロードについてです。Kotlinでは既に用意されている演算子記号に対応するシグネチャインスタンス関数(あるいは拡張関数)を定義することにより演算子オーバロードを実現しています。

class MyInt(val value : Int) {
  
  fun plus(that : MyInt) = MyInt(value + that.value)
  
  fun toString() = "$value"
}
 
fun main(args : Array<String>) {
  val five = MyInt(5)
  val seven = MyInt(7)
  
  println(five.plus(seven)) // 通常の関数呼び出し
  println(five plus seven)  // 中置呼び出し
  println(five + seven)       // 演算子
}

演算子とそれに対応する関数シグネチャの一覧は次のURIからご覧できます。

http://confluence.jetbrains.net/display/Kotlin/Operator+overloading

ジェネリック関数

Kotlinの関数はJavaと同様に型引数を宣言することにより、ジェネリック関数となり、型を柔軟に扱えます。

fun <T> head(array : Array<T>) : T = array[0]
 
fun last<T>(array : Array<T>) = array[array.size - 1]

上記の例のように関数 head のようなスタイルと last のようなスタイルがあります。単に型引数 <T> をどこに記述するかの違いに過ぎません。ジェネリクスの詳細は後日お話しします。

まとめと次回予告

今日は関数について多くのことを学びました。関数リテラルは関数をキーワード fun を用いずに直接、使用箇所に記述できる関数です。関数リテラルは変数や引数に渡すことができます。高階関数は、引数や返り値に関数リテラルを取ることができます。高階関数と関数リテラルにより、抽象度の高い関数が定義できるとともに、その呼び出しコードは簡潔で読みやすいです。その他に、中置呼び出し、クロージャ、インライン関数、演算子オーバロード、ジェネリック関数を見ました。

明日からはお待ちかね、オブジェクト指向的な機能について学んでいきます。明日はクラスについて扱います。

日記

昨日は学生時代のアルバイト仲間と忘年会をして来ました。