0
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】Kotlin DSL設計 — 受容者付き関数型で作る型安全な内部DSL

Posted at

はじめに

Kotlin の最大の強みの一つが DSL(Domain Specific Language) を簡潔に構築できることです。

本記事では、「受容者付き関数型(Function Type with Receiver)」を核に、
Gradle や Jetpack Compose のような宣言的・型安全なDSLを自作する方法を紹介します。


1. DSLとは何か

DSL(Domain Specific Language) とは、
特定のドメインや用途に特化した小さな言語です。

Kotlin は柔軟な関数型構文を持ち、
「宣言的・可読性の高い構文」を自然に表現できるため、
Gradle や kotlinx.html、Jetpack Compose のような DSL 実装に最適です。

例:Gradle 風の宣言

dependencies {
    implementation("org.jetbrains.kotlin:kotlin-stdlib")
}

2. 受容者付き関数型(Function Type with Receiver)

Kotlin DSLの根幹をなすのが「受容者付き関数型」です。

class Robot {
    fun forward(step: Int) = println("→ $step")
    fun turnRight() = println("↱ turn right")
}

fun robot(block: Robot.() -> Unit): Robot {
    return Robot().apply(block)
}

fun main() {
    robot {
        forward(3)
        turnRight()
        forward(1)
    }
}

Robot.() -> Unit は「Robot をレシーバに持つラムダ」。
block の中では this が暗黙的に Robot となり、
ドメイン語彙だけで表現できる。


3. @DslMarker によるスコープ衝突防止

複数のビルダーをネストすると、外側・内側のプロパティが混ざる危険があります。
これを防ぐために @DslMarker アノテーションを付与します。

@DslMarker
annotation class UiDsl

@UiDsl
class ScreenBuilder {
    var title = ""
    fun button(label: String) = println("button: $label")
}

@UiDsl
class AppBuilder {
    fun screen(block: ScreenBuilder.() -> Unit) {
        ScreenBuilder().apply(block)
    }
}

fun app(block: AppBuilder.() -> Unit) = AppBuilder().apply(block)

fun main() {
    app {
        screen {
            title = "Home"
            button("Start")
            // title = "Oops" ← 外のスコープのtitleを参照できない(安全!)
        }
    }
}

@DslMarker を付けると、同一DSL階層での
「スコープ混在(shadowing)」を防止でき、補完も安全に。


4. Kotlin 2.0 以降の context receiver

Kotlin 2.x では、複数レシーバを自然に扱える context receiver が導入されました。

例:DBセッションとトランザクションを同時スコープに持つ DSL

class DbSession { fun query(sql: String) = println("run: $sql") }
class Tx { fun commit() = println("commit") }

context(DbSession, Tx)
fun selectOne(sql: String) {
    query(sql)
    commit()
}

fun <R> withDb(block: context(DbSession, Tx) () -> R): R {
    val db = DbSession()
    val tx = Tx()
    return with(db, tx) { block(db, tx) }
}

fun main() {
    withDb {
        selectOne("SELECT * FROM users")
    }
}

context により:

  • 両方の API (DbSession, Tx) に補完アクセス可能
  • 型安全かつネスト不要

5. ビルダーパターンとの融合(典型構造)

DSL の多くは「Builder + apply + build()」構造で設計されます。

data class Button(val label: String)
data class Screen(val title: String, val buttons: List<Button>)

@DslMarker
annotation class ScreenDsl

@ScreenDsl
class ScreenBuilder {
    var title: String = "Untitled"
    private val buttons = mutableListOf<Button>()

    fun button(label: String) { buttons += Button(label) }
    fun build(): Screen = Screen(title, buttons)
}

fun screen(block: ScreenBuilder.() -> Unit): Screen =
    ScreenBuilder().apply(block).build()

fun main() {
    val s = screen {
        title = "Dashboard"
        button("OK")
        button("Cancel")
    }
    println(s)
}

出力:

Screen(title=Dashboard, buttons=[Button(label=OK), Button(label=Cancel)])

build() で不変化し、
DSL は可読性と再利用性の両立が可能になります。


6. operator / infix による構文糖衣

受容者付き関数型を operator と組み合わせると、
より自然言語的な構文にできます。

@JvmInline
value class Col(val name: String)

class Where {
    private val conds = mutableListOf<String>()
    infix fun Col.eq(value: Any) { conds += "$name = '$value'" }
    infix fun Col.gt(value: Any) { conds += "$name > '$value'" }
    fun build() = conds.joinToString(" AND ")
}

fun where(block: Where.() -> Unit): String =
    Where().apply(block).build()

fun main() {
    val cond = where {
        Col("age") gt 18
        Col("name") eq "Anna"
    }
    println(cond)
}

出力:

age > '18' AND name = 'Anna'

infix@JvmInline による表現力強化は、SQL や HTML DSL に最適。


7. 設計パターンとベストプラクティス

パターン 概要
Type-safe builder 不変モデルを安全に構築(Gradle, kotlinx.html)
Scoped DSL @DslMarker でスコープ境界を明確化
Functional Composition apply, run, also で関数合成
Immutable Build Result build() で確定モデルを返す
Context receiver 複数の文脈を安全に共有
Extension-based Expansion fun Builder.chart { ... } で拡張可能

DSL は「読めるコード」こそ最優先。
過度な operator やネストは可読性を損なうため、
宣言的・直感的にすることが最重要。


まとめ

要素 キー概念
設計基盤 T.() -> R(受容者付き関数型)
安全性 @DslMarker
柔軟性 context receiver(Kotlin 2.x)
拡張性 operator, infix, extension function
不変性 build() パターン

Kotlin DSL は「構文を設計する」という、
APIデザインの最前線にある技術です。
GradleやComposeのように、
ドメイン知識をそのままコードに落とし込むための表現力が得られます。

0
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
0
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?