「Kotlin DSL」という言葉、聞いたことはありますか?
Gradle の build.gradle.kts や Jetpack Compose など、
Kotlin の世界では頻繁に登場する書き方です。
// これが Kotlin DSL !
html {
head {
title("My Website")
}
body {
h1 { +"Hello, World!" }
p { +"Kotlin DSL is awesome." }
}
}
一見すると「 言語の構文?」に見えますが、実はこれ ただの Kotlin のコード なんです。
今回は、この「魔法のような書き方」を作るための たった2つの仕組み を解説します。
なぜ DSL を使うの? 🤔
Java 時代によくあった Builder パターンと比べてみましょう。
従来の Builder パターン:
// 階層構造が見にくい...
new HtmlBuilder()
.setHead(new HeadBuilder().setTitle("My Website").build())
.setBody(new BodyBuilder().addH1("Hello").build())
.build();
Kotlin DSL:
Kotlin DSL:Kotlin// 構造がそのままコードに!
html {
head { title("My Website") }
body { h1 { +"Hello" } }
}
圧倒的に 「可読性」 が高いですよね。しかも、コンパイル時に型チェックが効くので、タグの閉じ忘れや構造ミスも IDE が教えてくれます 。
仕組みはたった2つだけ!
🛠️この書き方を実現しているのは、以下の2つの機能の組み合わせです。
1. 高階関数と末尾ラムダ (Trailing Lambda)
2. レシーバ付きラムダ (Function Literal with Receiver)
これだけだと難しいので、順番に見ていきましょう。
1. カッコ () を省略する「末尾ラムダ」
Kotlin では、関数の最後の引数がラムダ式のとき、カッコの外に出せる というルールがあります
Kotlin// 定義
fun html(block: () -> Unit) {... }
// 呼び出し(普通)
html({ println("hello") })
// 呼び出し(末尾ラムダ)← これで DSL っぽくなる!
html {
println("hello")
}
2. そのブロック、誰のもの?「レシーバ付きラムダ」
これが DSL の 一番の肝 です。
ラムダ式 block: () -> Unit を block: HTML.() -> Unit に書き換えるだけです。
-
() -> Unit: ただの関数 -
HTML.() -> Unit: HTMLクラスの中で実行しているかのように振る舞う 関数
このブロックの中では、this が HTML オブジェクトを指すようになります。
Kotlinclass HTML {
fun body() { println("body created!") }
}
// レシーバ付きラムダを受け取る関数
fun html(block: HTML.() -> Unit) {
val html = HTML()
html.block() // ここで実行!
}
// 使うとき
html {
// ここは HTML クラスの内部扱い!
// だから `this.body()` と書かなくても body() が呼べる
body()
}
実践:3ステップで HTML ビルダーを作ろう 🚀
では、実際に動くコードを書いてみましょう。
ステップ1:タグのクラスを作るまずは入れ物を用意します。
Kotlinopen class Tag(val name: String) {
private val children = mutableListOf<Tag>()
// 子要素を追加して初期化するヘルパー関数
protected fun <T : Tag> initTag(tag: T, init: T.() -> Unit): T {
tag.init() // ラムダを実行(設定など)
children.add(tag) // 自分(親)の子リストに追加
return tag
}
// 文字列化(再帰的に呼ぶ)
override fun toString() = "<$name>${children.joinToString("")}</$name>"
}
class HTML : Tag("html") {
// HTML の中には body が書ける
fun body(init: Body.() -> Unit) = initTag(Body(), init)
}
class Body : Tag("body") {
// Body の中には h1 が書ける
fun h1(init: H1.() -> Unit) = initTag(H1(), init)
}
class H1 : Tag("h1")
ステップ2:エントリポイントを作る
最初に呼び出す関数です。
Kotlinfun html(init: HTML.() -> Unit): HTML {
val root = HTML()
root.init()
return root
}
ステップ3:文字列のトリック
unaryPlus+"Hello" のように書くための小技です。String クラスに unaryPlus(単項プラス演算子)を拡張します。
Kotlin// Tag クラスに追加
open class Tag(val name: String) {
//... (省略)...
// 文字列に "+" をつけると、テキストノードとして追加される魔法
operator fun String.unaryPlus() {
children.add(TextElement(this)) // ※TextElementは別途定義した簡単なクラス
}
}
完成! 🎉
これで、冒頭のコードが動くようになります。
Kotlinval text = html {
body {
h1 { +"Kotlin DSL!" }
}
}.toString()
println(text)
// 出力: <html><body><h1>Kotlin DSL!</h1></body></html>
一歩進んだテクニック:@DslMarker で安全に 🛡️
ネストが深くなると、間違って外側のメソッドを呼んでしまう問題(Scope 漏れ)が起きます。
Kotlinhtml {
body {
// body の中に body は書けないはずなのに、コンパイルが通ってしまう!
// (外側の html ブロックが見えているため)
body { }
}
}
これを防ぐのが @DslMarker です。
Kotlin@DslMarker
annotation class HtmlTagMarker
@HtmlTagMarker
open class Tag(val name: String)...
これをつけるだけで、一番近いレシーバ(this)以外のメソッド呼び出しをコンパイラが禁止してくれます。
まとめ
Kotlin DSL を作るための重要ポイントはこれだけです。
-
レシーバ付きラムダ
T.() -> Unitでthisのコンテキストを切り替える - 拡張関数 や 演算子オーバーロード で書きやすくする
-
@DslMarkerで安全性を確保する
自分のプロジェクトで「設定ファイル」や「複雑なオブジェクト生成」があったら、ぜひ 「俺々DSL」 を作って、チーム開発の効率を爆上げしてみてください!🚀