stdlibと拡張関数とコレクション
Kotlinのstdlibはほぼ拡張関数の集まりなのですが、Javaにはプリミティブ型があり、それらは型パラメータとして扱えないため、コレクションに対して拡張関数を書くには以下のように型パラメータ、byte、short、int、long、float、double、boolean、charに対して似たようなコードを繰り返し書かなければなりません。
/**
* Returns `true` if the array is empty.
*/
@kotlin.internal.InlineOnly
public inline fun <T> Array<out T>.isEmpty(): Boolean {
return size == 0
}
/**
* Returns `true` if the array is empty.
*/
@kotlin.internal.InlineOnly
public inline fun ByteArray.isEmpty(): Boolean {
return size == 0
}
/**
* Returns `true` if the array is empty.
*/
@kotlin.internal.InlineOnly
public inline fun ShortArray.isEmpty(): Boolean {
return size == 0
}
/**
* Returns `true` if the array is empty.
*/
@kotlin.internal.InlineOnly
public inline fun IntArray.isEmpty(): Boolean {
return size == 0
}
/**
* Returns `true` if the array is empty.
*/
@kotlin.internal.InlineOnly
public inline fun LongArray.isEmpty(): Boolean {
return size == 0
}
/**
* Returns `true` if the array is empty.
*/
@kotlin.internal.InlineOnly
public inline fun FloatArray.isEmpty(): Boolean {
return size == 0
}
/**
* Returns `true` if the array is empty.
*/
@kotlin.internal.InlineOnly
public inline fun DoubleArray.isEmpty(): Boolean {
return size == 0
}
/**
* Returns `true` if the array is empty.
*/
@kotlin.internal.InlineOnly
public inline fun BooleanArray.isEmpty(): Boolean {
return size == 0
}
/**
* Returns `true` if the array is empty.
*/
@kotlin.internal.InlineOnly
public inline fun CharArray.isEmpty(): Boolean {
return size == 0
}
stdlibのコード生成
そこで、Kotlinのstdlibではコレクションに対する拡張関数をテンプレートからコード生成しており、生成されたコードは libraries/stdlib/src/genereted/ に置かれます。
例えば上の isEmpty のテンプレートは libraries/tools/kotlin-stdlib-gen/src/templates/Arrays.kt に以下のように定義されています。
templates add f("isEmpty()") {
inline(Inline.Only)
only(ArraysOfObjects, ArraysOfPrimitives)
doc { "Returns `true` if the array is empty." }
returns("Boolean")
body {
"return size == 0"
}
}
少しトリッキーに見えますが f の定義を見てみると
fun f(signature: String, init: GenericFunction.() -> Unit) = GenericFunction(signature).apply(init)
となっており、ブロックのレシーバーが GenericFunction で、そのクラスがメソッドのビルダーになっていることが分かります。
テンプレートからコードを生成するには以下のコマンドを実行します。
$ cd libraries/tools/kotlin-stdlib-gen
$ mvn compile exec:java
これで libraries/stdlib/src/generated/Arrays.kt に isEmpty が生成されます。
stdlibのコレクションに拡張関数を追加する
実際に peek という、デバッグのためにリストの中身を覗き見るためのメソッドを追加してみます。
listOf(1, 2, 3)
.map { ... }
.peek { println(it) } //=> 1, 2, 3
.map { ... }
ビルダーのメソッドの一部を紹介します。
| name | what |
|---|---|
| inline | 生成されるメソッドをインラインにするかを指定します |
| typeParam | 型パラメータを指定します |
| only | どのコレクション(Familyと呼ばれています)に対して拡張関数を定義するかを指定します |
| returns | 返り値の型を指定します |
| body | メソッドの中身はここに書きます |
雑な peek の実装は以下のようになります。
templates add f("peek(action: (T) -> Unit)") {
inline(true)
only(Lists)
typeParam("T")
returns("List<T>")
body {
"""
for (item in this)
action.invoke(item)
return this
"""
}
}
テンプレートの定義は独自記法で少しクセがあるので先に generated の方に実装したいメソッドを書いてみて、動いたらテンプレートに移すのが良いと思われます。
書けたらビルドして実行してみます。
$ mvn compile exec:java
$ ant runtime
$ dist/kotlinc/bin/kotlinc
Type :help for help, :quit for quit
>>> listOf(1,2,3).peek { println(it) }
1
2
3
[1, 2, 3]
Familyについて
Family はどのクラスに対して生成するのか対応になっています。
templates の only で Family 指定します。デフォルトは全てのFamilyに対して生成するようになっており、対応は以下の通りです。
| Family | packageName | SourceFile |
|---|---|---|
| Iterables | "kotlin.collections" | Collections |
| Collections | "kotlin.collections" | Collections |
| Lists | "kotlin.collections" | Collections |
| Sets | "kotlin.collections" | Sets |
| Maps | "kotlin.collections" | Maps |
| InvariantArraysOfObjects | "kotlin.collections" | Arrays |
| ArraysOfObjects | "kotlin.collections" | Arrays |
| ArraysOfPrimitives | "kotlin.collections" | Arrays |
| Sequences | "kotlin.sequences" | Sequences |
| CharSequences | "kotlin.text" | Strings |
| Strings | "kotlin.text" | Strings |
| Ranges | "kotlin.ranges" | Ranges |
| RangesOfPrimitives | "kotlin.ranges" | Ranges |
| ProgressionsOfPrimitives | "kotlin.ranges" | Ranges |
| Generic | "" | Misc |
| Primitives | "" | Misc |
peek の例では only に Lists を指定したので libraries/stdlib/src/generated/_Collections.kt にメソッドが生成された、という感じです。