はじめに
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のように、
ドメイン知識をそのままコードに落とし込むための表現力が得られます。