1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Kotlin】5分でわかる!魔法のような「Kotlin DSL」の作り方

1
Posted at

「Kotlin DSL」という言葉、聞いたことはありますか?

Gradle の build.gradle.ktsJetpack 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: () -> Unitblock: HTML.() -> Unit に書き換えるだけです。

  • () -> Unit : ただの関数
  • HTML.() -> Unit : HTMLクラスの中で実行しているかのように振る舞う 関数

このブロックの中では、thisHTML オブジェクトを指すようになります。

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 を作るための重要ポイントはこれだけです。

  1. レシーバ付きラムダ T.() -> Unitthis のコンテキストを切り替える
  2. 拡張関数演算子オーバーロード で書きやすくする
  3. @DslMarker で安全性を確保する

自分のプロジェクトで「設定ファイル」や「複雑なオブジェクト生成」があったら、ぜひ 「俺々DSL」 を作って、チーム開発の効率を爆上げしてみてください!🚀

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?