目次
Kotlin での DSL の書き方についてまとめました。
Kotlin は 内部の分離されたDSL作成を支援してくれます。 DSL作成では特に Lambda を活用することになります。 DSL の分類について知りたい方は DSL, DOMAIN SPECIFIC LANGUAGE の分類 をご参照ください。
DSLを作成する際の書き方はいくつかありますが、 Kotlin のドキュメントの TypeSafeBuilders に倣ってクラスを作成します。 他に、 Kotlin DSL from Theory to Practice のように Builderクラスを作って組み合わせるやり方もあります。
サンプルコード
DSL を記述するためのクラス・関数を用意します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class A { var valueA: String? = null var b: B? = null fun b(init: B.() -> Unit): B { val b = B() b.init() this.b = b return b } } class B { var valueB: String? = null var c: C? = null fun c(init: C.() -> Unit): C { val c = C() c.init() this.c = c return c } } class C { var valueC: String? = null } fun a(init: A.() -> Unit): A { val a = A() a.init() return a } |
このように書くと、次のように クラスA のオブジェクトを作ることができます。
1 2 3 4 5 6 7 8 9 |
a { valueA = "A" b { valueB = "B" c { valueC = "C" } } } |
上記サンプルコードの問題点
上のサンプルコードに DSL の基本が詰まっています。 しかしこのままでは、次のようなコードも記述可能です。 ルール上可能ですが、非常に見通しが悪くなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
a { valueA = "A" b { valueB = "B" c { valueA = "A" valueC = "C" } b { valueA = "A" valueB = "B" b { valueB = "B" } } } } a { b { valueB = "B" valueA = "A" } } a { b { c { valueC = "C" valueB = "B" valueA = "A" } } } |
具体的な問題項目は次の通りです。
- B, C を定義するブロックの中で A の属性が代入されている。
- B を定義するブロックの中で、さらに B を定義する関数とそのブロックが記述されている。
@DslMarker
Kotlin 1.1 から導入されているアノテーション @DslMarker
を用いて上の問題を解決できます。 @DslMarker
はDSLを作成する際の、スコープを制御するために使えるアノテーションです。 このアノテーションは、アノテーションクラスに付与します。
@DslMarker
を付けたアノテーションクラス(DslMarkerTest
)を作成し、 DSLを生成するクラスに付与します。 クラス A, B にアノテーションDslMarkerTest
を付与します。
1 2 3 4 5 6 7 8 9 10 |
@DslMarker annotation class DslMarkerTest @DslMarkerTest class A { ... } @DslMarkerTest class B { ... } class C { ... } |
このようにすると、次のようなコードはエラーとなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
a { b { valueB = "B" valueA = "A" // Error } } a { b { c { valueC = "C" valueB = "B" // Error valueA = "A" // Error } } } |
@DslMarker
を付与したアノテーション @DslMarkerTest
を付与した2つ以上のクラスのオブジェクトが暗黙的レシーバ、すなわち this
になりうる場合、それらのオブジェクトのうち一番最後に this
になるオブジェクト以外は this
として使えません。
上のサンプルコードでは、 メソッドaに渡すラムダの中では @DslMarkerTest
のついたクラスAのインスタンスが this
となっていますが、 this.b
に渡すラムダの中では新しく @DslMarkerTest
のついたクラスBのインスタンスが this
となるため、 その前に this
となっていた クラスAのインスタンスは this
としては使えなくなります。