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

Kotlin Advent Calendar 2012 (全部俺)

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

18日目:パッケージと標準ライブラリ

f:id:ngsw_taro:20121218213136j:plain

今日はKotlinの標準ライブラリが提供する便利なクラスや関数について紹介します。ライブラリは、自然言語のメタファを拝借するなら、語彙と例えられることがあります。ライブラリの提供する機能群(語彙)を知ることにより、表現豊かなコードを書くことができるようになります。

パッケージ

標準ライブラリの紹介の前にKotlinにおける名前空間のお話をします。

KotlinではJavaと同様に名前空間パッケージによって区切ることができます。名前空間を区切ることは、クラスなどの名前(完全修飾名)の衝突を避け、世界中でユニークなものにできたり、類似した機能をまとめたりといったメリットがあることは、Javaプログラマにとっては釈迦に説法ですね。

パッケージを定義するには、これもJavaと同じで、ソースファイル先頭にキーワード package を記述します。その後に続けてパッケージ名を書いていきます。必要があればドットを打ってパッケージを区切ることもできます。

package com.taroid.sample

Kotlinでは、パッケージはファイルシステムと対応していなくても問題ありません。Javaでは例えば、上記のようなパッケージを定義したら、その配下にあるクラスのソースファイルは次のようなパスのディレクトリに格納しなければならないというルールがあります。

<プロジェクトルート>/com/taroid/sample/

Kotlinにはこのようなルールは存在しません。

インポート

パッケージ内に定義されているクラスにアクセスする場合は、完全修飾名を指定するか、パッケージをインポートする方法があります。これもJavaと同様です。

インポートするときに次のようにキーワード as を付けるとインポート対象に別名を付けることができます。

package foo

import bar.Bar as Hoge

fun main(args : Array<String>) {
    Hoge()
}

標準ライブラリ

では、Kotlinの標準ライブラリの提供するクラス、関数について見ていきたいと思います。デフォルトでは*1パッケージ kotlin とパッケージ jet *2内のクラスや関数は暗黙的にインポートされているようです*3

リストと配列

クラス Array のコンストラクタ

Kotlinでは配列は特別な存在ではなく、単なるジェネリッククラスです。そのコンストラクタは引数を2つ取ります。1つ目が配列の長さ。もう1つが、配列初期化のための関数を取ります。次のコードは5つの要素を持った配列を生成しています。配列の要素は先頭から、0、2、4、5、6、8です。

  val array = Array(5) {
    it * 2
  }
  
  array forEach {
    print(it)
  }

各要素を走査する関数 forEach も上記例で使用しています。リストクラスやレンジクラスも同様の関数を持っています。関数 forEach は引数に「要素を引数に取る関数」を取ります。上記コードを実行すると標準出力に「02468」と書き出します。

リストのファクトリ関数

リストのファクトリ関数として listOf、arrayLisfOf、linkedListOf があります。どれも引数は可変長のジェネリック型 Tを取ります。listOf はイミュータブルなリストを返します(型は jet.List<out T>)*4。arrayListOf と linkedListOf はそれぞれ java.util.ArrayListjava.util.LinkedList のインスタンスを生成し、返します。これらはご存知の通りミュータブルです。

val ints : jet.List<Int> = listOf(1, 2, 3)
val nums : jet.List<Number> = ints
  
println(nums[0])   // 1
println(nums.size) // 3

リストの要素には、配列のように角括弧の中にインデックスを指定する形式でアクセスすることができます。また、プロパティ size により要素数を取得することができます。

リストの要素を判定

リストや配列には関数 all と any という便利な関数が用意されています。それぞれ引数には関数を取ります。その型は ( T ) -> Boolean で、その引数にはリストの要素が順に与えられていきます。このように Boolean を返す関数リテラルのことを述語と呼ぶことがあります。

リストの関数 all を呼び出したとき、与えた述語の評価結果がすべての要素に対して true の場合に限り、all は true を返します。それ以外の場合は false を返します。逆に関数 any は、述語の評価結果が1度でも true なら true を返し、それ以外の場合に false を返します。

val langs = listOf("java", "kotlin", "groovy")

val a = langs.any {
    it.size >= 8
}

println(a) // false

val b = langs.all {
    it.toCharArray().all {
        it.isLowerCase()
    }
}

println(b) // true

何か面白いことをして新しいリストを得る

関数 filter はその名前の通り、リストの各要素を任意のルールでフィルタリングして新たなリストを返す関数です。関数 map はリストの各要素を取り、なんらかの変換を施した新しい要素から構成されるリストを返す関数です。

val ints = listOf(1, 2, 3, 4, 5)

val filteredInts = ints.filter {
    it % 2 == 0
}

println(filteredInts) // [2, 4]

val mappedInts = ints.map {
    it * 2
}

println(mappedInts)   // [2, 4, 6, 8, 10]

畳み込み関数

畳み込み関数は、リストの各要素を1つの値にまとめたいときに使用します。例えばInt型リストの各要素の総和を求めたい場合などです。畳み込んでいく順番は結果に影響しますので、左(リストの先頭)からの畳み込みには関数 fold を、右からの畳み込みには関数 foldRight を使用します。 関数 fold のとる引数は2つです。1つ目が初期値です。2つ目が関数で型は (R, T) -> Tで、1つ目の引数は現時点での畳み込み結果、2つ目の引数はリストの要素です。関数 foldRight の取る引数は fold と数と種類は同じですが、順番が逆です。

val words = "i live in tokyo".split(' ')

val leftResult = words.fold("") {
    (a : String, e : String) -> "$a $e"
}

println(leftResult)  // i live in tokyo

val rightResult = words.foldRight("") {
    (e : String, a : String) -> "$a $e"
}

println(rightResult) // tokyo in live i

マップ

マップを生成するには hashMapOf、sortedMapOf、linkedMapOf といった関数が用意されています。引数には可変長のペアを取ります。ペアはクラス Pair で表現される2つの値を持つことができるコンテナです。ペアは次のように生成することができます。

val pair = Pair("key", "value")

しかし特にマップに渡すようなペアである場合は関数 to を使用したほうがすっきりと記述することができます。

val map = hashMapOf("key" to "value")

このマップに対して、キーから値を取得したい場合やキーに対して値を設定したい場合は連想配列ライクに記述することができます。

println(map["key"]) // value

関数の事前検証、事後検証用関数 2012-12-28追記

プログラム(具体的には関数など)が正しく動作するために満たされるべき条件(仕様)をコード中に記述し、間違いを防ごうとする手法を契約による設計と言ったりします。簡単に言えば、関数が正しく動く条件が整っているか(オブジェクトの状態や引数が適切であるか)、関数は正しく動作を終えたか(関数呼び出しにより期待する状態を得られたのか)などの条件をコードで表現します。

具体例を見るとシンプルなアイデアであることがわかります。クラス Rectangle は長方形を表現しているクラスです。コンストラクタは長方形の幅と高さを引数として受け取ります。幅と高さは0より大きい値を期待します。もし0以下の値が渡されたら、不正な状態の長方形オブジェクトを生成することになってしまいます。それを防ぐためのコードを次に示します。

class Rectangle(width : Double, height : Double) {
  val width : Double
  val height : Double
  
  {
    require(width > 0.0)
    require(height > 0.0,
      "height should be greater than 0, but actual it's $height.")
    
    this.width = width
    this.height = height
  }
}

fun main(args : Array) {
  val a = Rectangle(4.0, 3.0)
  val b = Rectangle(1.0, 0.0) // throw EXCEPTION!!!
}

関数 require は第1引数にBoolean値を取ります。この値が false の場合、例外 IllegalArgumentException を投げます。例外へ渡すメッセージを第2引数に指定することもできます。実行時例外を投げることは乱暴のような気もしますが「誤りはすぐに知らせる」という原則に基づけば理に適っています*5。ちなみに、第2引数には例外メッセージとして、文字列を返す関数リテラルを指定することもできます。単に文字列を指定するバージョンとの違いは、評価のタイミングです。関数リテラルを渡すバージョンは例外を投げる直前に評価されます(文字列を渡すバージョンは、常に評価される)。

標準ライブラリが提供するこのような関数を以下の表にまとめます。

関数対応する例外
assert AssertionError
require IllegalArgumentException
requireNotNull IllegalArgumentException
check IllegalStateException

まとめと次回予告

今日はKotlinにおけるパッケージと標準ライブラリの提供する一部の機能について紹介しました。パッケージはJavaのそれと似ていますが、ところどころ異なるので注意が必要です。標準ライブラリはまだまだ紹介していないクラスや関数がたくさんあるので、明日以降もちょいちょい紹介していきたいと思います。

明日はまだ紹介していないオブジェクト指向に関する機能についてお話ししたいと思います。

日記

最近、昼食はラーメンばっかり...。

*1:Kotlinにはモジュールという概念があり、それはビルド単位です。そのビルドスクリプト内で事前にインポートするということができるようです。このモジュールという機能は現在、再設計中のようです。

*2:パッケージ jet内のクラス は正確には標準ライブラリというよりも組込みクラスのような感じがします。ただしパッケージ jet 内のクラスの「拡張関数」はパッケージ kotlin 内にあります。

*3:ここの根拠となるような文書は見つかりませんでした。Kotlinの言語仕様は公開されていません(たぶん)(というかまだ確定していないだけかも)。

*4:現時点では返されるオブジェクトの実体はjava.util.ArrayListインスタンスです。しかし、このことに依存するコードを書くべきではありません。

*5:もし実行時例外を投げず、不正な値を許可したらどうなるでしょう。ここでの誤りが形として現れるのは、コード上のどこか遠いところです。そこで初めて間違いに気付くのでデバッグは困難です。