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

算譜王におれはなる!!!!

偏りはあると思うけど情報技術全般についてマイペースに書くよ。

KotlinでDSLを作るときに活躍する5大言語機能

Kotlin プログラミング

メリークリスマス!とか言いつつ、このエントリは、Kotlin Advent Calendar 2013の2日目分のエントリです。空きを放置はしたくないので過去分をやります。まだ枠はあるのでやりたい方はぜひ!!

Kotlinは内部DSL (Internal Domain Specific Language) を記述するのに適した言語です。 Kotlinのシンプルな文法や、複雑すぎない構文糖衣のルールが絶妙にマッチし、さらに静的言語なのでコンパイルタイムに型や文法由来の誤りが検知されてランタイムのつまらないバグを排除できます。 本エントリでは、KotlinでDSLを作る上で非常に役立つ言語機能について紹介します。

その1 中置呼び出し (Infix call)

中置呼び出しという構文糖衣があります。次の2つのメソッド呼び出しは等価です。

"Kotlin".compareTo("Java")
"Kotlin" compareTo "Java"

後者では、ドットと引数の括弧が省略されています。 「レシーバ、メソッド、引数1つ」となるようなメソッド呼び出しの場合に限り中置呼び出しによる記述が可能です。 println "Hello"のような書き方はできません。System.out println "Hello"ならOKです。

Kotlinでコードゴルフする際にも役に立つので覚えておきましょう。

その2 拡張関数

拡張関数は既存のクラスにメソッドを追加できる機能、構文です。Stringクラスにshowメソッド追加してみます。

fun String.show() {
    println(this)
}

通常の関数、メソッドのようにfunキーワードを伴って定義します。この拡張関数がスコープにある箇所ではshowStringのメソッドのように扱えます。

"Merry Christmas".show() // => Merry Christmas

同様に、拡張プロパティを定義することもできます。

val String.upperCaseNum: Int
get() = filter { it.isUpperCase() }.size
"Merry Christmas".upperCaseNum // => 2

DSLに応用する例としては10000.yenとか8.pxとか単位を表現するとわかりやすいですね。

その3 演算子オーバロード

Kotlinは演算子オーバロードをサポートしています。複雑さを避けるためかGroovyのように、演算子に対応した名前を持つメソッドを定義することで演算子オーバロードを実現しています。2項演算子+は、ひとつの引数を取るplusメソッドに対応する、という具合です。

StringBuilder+=演算子を追加する例です。これは拡張関数です。

fun StringBuilder.plusAssign(value: Any?) {
    append(value)
}
val builder = StringBuilder("Hello, ")
builder += "World!"
println(builder) // => Hello, World!

演算子オーバロードと拡張プロパティを組み合わせて5.hours - 30.minutesという計算を簡単に表現できます。

その4 高階関数 + 関数リテラル

Kotlinでは関数オブジェクトを引数として取る高階関数が多く登場します。たいていの場合は、関数オブジェクトをその場で定義、生成する関数リテラルが便利です。関数リテラルは必ず{}が必要です。

整数リストを畳み込んでリストの合計値を求めたいとき、次のようなコードを記述します。

listOf(1, 2, 3).fold(0, {
    (a, e) -> a + e
})

foldメソッドは引数を2つ取ります。最初の引数はアキュムレータの初期値、2つ目の引数は現在のアキュムレータの値と次のリスト要素を受け取ってなんかしらの値を返す関数オブジェクトです。

このような関数オブジェクトを引数にとる関数は多いので構文糖衣が用意されています。上記のコードは次のコードと等価です。

listOf(1, 2, 3).fold(0) {
    (a, e) -> a + e
}

関数リテラルの記述場所が、引数リストの括弧の外になりました。最後の引数が関数オブジェクトを取る場合に限り、この構文糖衣は有効です。

他の例として標準ライブラリにあるtimesというIntの拡張関数を見てみます。これはレシーバとなる数値分だけtimesの引数の関数を呼び出すメソッドです。

3 times { print("wa") }

このコードを実行するとwawawaと出力されます。 単なる標準ライブラリの関数呼び出しが、まるで組み込み構文のように見えます。

その5 拡張関数リテラル

最後に紹介するのは拡張関数リテラルです。実際にこのような名前で呼ばれているかはわかりませんが、要は拡張関数と関数リテラルの組み合わせです。ちょっぴり難しいかも知れませんが、面白いことができます。

JavaScriptとかVBのwith構文っぽいものが欲しいとします。

val immutableArrayListBuilder = ImmutableArrayListBuilder<String>()
with(immutableArrayListBuilder) {
    add("hoge")
    add("fuga")
    add("piyo")

    println(this == immutableArrayListBuilder) // => true
}
    
println(immutableArrayListBuilder.build()) // => ["hoge", "fuga", "piyo"]

このように長い変数名を省略できて読みやすいですね(たぶん...)。しかし残念ながらwith構文はKotlinにはありません。が、簡単に実装できます。

fun <T> with(receiver: T, f: T.() -> Unit) {
    receiver.f()
}

これだけです。

最初の引数として、ブロック内(正確には関数オブジェクト内)でレシーバとして扱いたいオブジェクトを渡します。2つ目の引数には、拡張関数オブジェクトを取ります。上記シグネチャを見るとわかりますが、関数オブジェクトfの型はT.() -> Unitです。これは「T型の拡張関数で、引数を取らずUnitを返す関数」と読めます。with本体ではfを呼び出すだけですが、fTの拡張関数なのでreceiver.f()と記述します。

ちなみに、このようなwith関数はKotlinの標準ライブラリに用意されているので自分で実装する必要はありませんよ*1

*1:例で示しているwithと標準ライブラリのそれはほんの少しだけ異なります。