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

Kotlin Advent Calendar 2012 (全部俺)

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

9日目:関数(前)

f:id:ngsw_taro:20121208142956j:plain

今日はいよいよ関数について学びます。Kotlinでは関数型プログラミング的アプローチによる記述が頻繁に登場します。そのため関数の紹介は量が多くなるので前編と後編の2部構成を採りたいと思います。

関数定義の基本

関数を定義するためには次のように記述します*1

fun 関数名 ( 引数リスト ) : 返値型 {
    関数本体
    return 返値
}

関数の定義にはキーワード fun が必要です。Javaではメソッド(ここでは関数と同義語としておきます)は必ずクラスに属するものでした。Kotlinではクラス内に関数を書くこともできますが、必ずしもその必要はありません

関数定義の具体的な例を次に示します。

fun max(a : Int, b : Int) : Int {
  return if(a > b) a else b
}

Kotlinの関数は必ず値を返します。特に関心を払うべき値を返さないような関数は、返り値としてUnit型Unit.VALUEを返します。それは、Javaのvoidに似ています。

fun greet() : Unit {
  println("Hello, world!")
  return Unit.VALUE
}

実はUnit型を返す関数の場合に限り、返値型、return、またはその両方を省略することができます。上記のコードはすっきりと次のように書けます。

fun greet() {
  println("Hello, world!")
}

単一式関数

例えば、最初に示した例の関数 max は、関数本体の中ですぐに式の評価結果を return しています。このような場合には、単一式関数という関数定義の構文糖衣があります。関数 max を単一式関数で表現すると次のようになります。

fun max(a : Int, b : Int) = if(a > b) a else b

関数シグネチャと式を等号で結んでいます。より簡潔で直感的な記述になりました。関数の返値型は型推論が働くため省略可能です。単一式関数によってプログラムを(手続き的ではなく)宣言的に書くことができます。

デフォルト引数と名前付き引数

Kotlinでは、関数の引数にデフォルトの値を設定することができます。デフォルト値が設定されている引数は、関数の呼び出し側でのパラメータ設定を省略できます。省略するとデフォルト値がパラメータとして使用されます。

fun greet(name : String = "world") {
  println("Hello, $name!")
}
 
fun main(args : Array<String>) {
  greet("Kotlin") // Hello, Kotlin!
  greet() // Hello, world!
}

ただし、次のような関数呼び出しはコンパイルエラーとなります。

fun add(a : Int = 0, b : Int) = a + b
 
fun main(args : Array<String>) {
  add(123) // ERROR!!!
}

パラメータは、引数の宣言順に指定する必要があるからです。つまり上記の123というパラメータは、引数 a に対するものであり、引数 b に対応するパラメータがないのでコンパイラは文句を言ってきます。これを解決する名前付き引数という仕組みがあります。次のように、引数名とパラメータを対応付けて関数を呼び出すことができます。

add(b = 123) // OK!

可変長引数

Kotlinにも関数が可変長の引数を取る仕組みが備わっています。可変長にしたい引数の名前の前にアノテーション vararg を付けます。すると、その引数には1つ以上のパラメータを設定できるようになります。

fun printlnAll(vararg strs : String) {
  for(any in strs) {
    println(any)
  }
}
 
fun main(args : Array<String>) {
  printlnAll("hoge", "fuga", "piyo")
}

可変長引数に、配列として一気に複数の値を渡すこともできます。その場合、次の例のように配列の前に * を記述する必要があります。

val array = array("hoge", "fuga", "piyo")
printlnAll(*array)

ローカル関数

ローカル関数、つまり関数内に関数を定義することができます。これは関数のスコープを絞りたいときに使用すると便利でしょう。

fun countUpperCase(s : String) : Int {
  fun _countUpperCase(s : List<Char>, n : Int) : Int =
    if(s.size == 0) n
    else _countUpperCase(s.tail, n + if(s.head!!.isUpperCase()) 1 else 0)
  
  return _countUpperCase(s.toCharList(), 0)
}
 
fun main(args : Array<String>) {
  val n = countUpperCase("AaaAaaA")
  println(n) // 3
}

拡張関数

拡張関数は便利で面白い機能です。既存のクラスなどに関数を追加する機能です。通常であれば、そのクラスを継承し、サブクラス内に関数を定義するでしょう。しかし拡張関数は、関数定義の場所を制限していません。つまり、クラスの外に拡張関数を定義することができます。用途としては、継承できないクラスに機能追加するなどです。

ローカル関数の例として挙げた関数 countUpperCase をStringの拡張関数として定義すると次のようになります。

fun String.countUpperCase() : Int {
  fun _countUpperCase(s : List<Char>, n : Int) : Int =
    if(s.size == 0) n
    else _countUpperCase(s.tail, n + if(s.head!!.isUpperCase()) 1 else 0)
  
  return _countUpperCase(toCharList(), 0)
}
 
fun main(args : Array<String>) {
  val n = "AaaAaaA".countUpperCase()
  println(n) // 3
}

実際には、レシーバとなるオブジェクトを第1引数として受け取る static メソッドとしてバイトコードにコンパイルされます。

再帰呼び出し

関数の再帰呼び出しJavaプログラミングでは、あまり見かけないかと思います。再帰呼び出しとは、関数が自分自身を呼び出すことです。上記のローカル関数 _countUpperCase がまさに再帰呼び出しを行っています。再帰呼び出しは、プログラムを宣言的に記述でき、また変数への破壊的代入が起こりにくいなどのメリットがあります。

練習問題解答例の改善

昨日掲載した練習問題の解答例を今日学んだ内容を活かして改善します。

FizzBuzz

fun Int.isFizz() = this % 3 == 0
fun Int.isBuzz() = this % 5 == 0
 
fun Int.fizzbuzz() = when {
      isFizz() && isBuzz() -> "fizzbuzz"
      isFizz() -> "fizz"
      isBuzz() -> "buzz"
      else -> "$this"
    }
 
fun main(args : Array<String>) {
  for(n in 1..100) {
    println(n.fizzbuzz())
  }
}

最大公約数

fun gcd(val a: Int,val b: Int) : Int = if(b == 0) a else gcd(b, a % b)

まとめと次回予告

今日は関数を紹介しました。単一式関数や再帰呼び出しを使えば関数型言語ライクにKotlinを使用できます。デフォルト引数、名前付き引数、ローカル関数など役に立つ仕組みがKotlinには導入されています。拡張関数により、既存クラスを操作する関数を可読性の高い形で提供できます。

明日は関数の紹介の続きをします。関数を抽象化するための仕組みである高階関数などについてお話しします。

日記

昨日はAndroidコミュニティの忘年会に行ってきました。

*1:必要があれば、キーワード fun の前にアノテーションを付けることも可能です。