Kotlin DSLについて

採用はこちら

Kotlin DSLとは、Kotlinの言語機能を使って、特定の目的に合った読みやすい記述スタイルを作る方法です。

ここでいうDSLは Domain Specific Language の略で、日本語では「特定分野向け言語」といった意味です。

たとえば、

  • 設定を書く
  • HTMLのような階層構造を書く
  • UIを宣言的に表現する
  • テストの意図を読みやすく書く

といった場面では、普通の手続き的なコードよりも、目的に特化した書き方のほうが分かりやすいことがあります。

Kotlin DSLは、そのような書き方をKotlinの中で実現するものです。

目次

DSLとは何か

DSLは、ある特定の用途に向けて設計された記述方法を指します。

たとえば以下はDSLの代表例です。

  • SQL
    → データベース操作向け
  • HTML
    → 文書構造の記述向け
  • CSS
    → スタイル指定向け
  • 正規表現
    → 文字列パターン指定向け

ただし、これらはKotlinとは別の言語です。

一方でKotlin DSLは、Kotlinそのものの構文や型システムを利用して作る内部DSLです。

つまりKotlin DSLは、Kotlinとは別の独立した新言語ではなく、Kotlinのコードとしてそのまま書けるDSLです。

Kotlin DSLの本質

Kotlin DSLの本質は、Kotlinの機能を使って、意図が伝わりやすい専用の記述スタイルを作ることです。

たとえば、普通の関数呼び出しだとこう書くかもしれません。

createPerson("Taro", 20, true)

これをDSL風にすると、次のように表現できます。

person {
    name = "Taro"
    age = 20
    active = true
}

後者の方が、

  • 何を設定しているのか
  • どの項目がどの値なのか
  • 構造がどうなっているのか

を読み取りやすくなります。

Kotlin DSLは、こうした読みやすさ・構造の分かりやすさ・型安全性を両立しやすいのが特徴です。

Kotlin DSLを支える主な機能

Kotlin DSLは、主に次のような言語機能によって成り立っています。

  • ラムダ式
  • レシーバ付きラムダ
  • 拡張関数
  • 拡張プロパティ
  • デフォルト引数
  • 名前付き引数
  • infix関数
  • 型安全なビルダー
  • @DslMarker によるスコープ制御

この中でも特に重要なのが、レシーバ付きラムダです。

もっとも重要な仕組み: レシーバ付きラムダ

Kotlin DSLを理解するうえで、最重要なのがこれです。

fun person(block: Person.() -> Unit): Person

この Person.() -> Unit は、Personをレシーバとして使えるラムダという意味です。

簡単にいうと、block の中では Person のメンバーをthis. を省略して自然に書けます。

たとえば次のようなコードです。

class Person {
    var name: String = ""
    var age: Int = 0
}

fun person(block: Person.() -> Unit): Person {
    val p = Person()
    p.block()
    return p
}

これを使うと、

val p = person {
    name = "Taro"
    age = 20
}

のように書けます。

この「あるオブジェクトを前提に、その中で自然に設定を書く」という仕組みがKotlin DSLの中心です。

Kotlin DSLの特徴

読みやすい

Kotlin DSLは、単なる関数呼び出しの羅列よりも、構造や意図を読み取りやすい形にしやすいです。

server {
    host = "localhost"
    port = 8080
}

この形は、設定情報をまとめて表現するのに向いています。

型安全

Kotlin DSLは通常のKotlinコードなので、型チェックを受けられます。

server {
    port = "abc"
}

のようなコードは、portInt を期待していればコンパイルエラーになります。

設定ファイルや構造記述を文字列ベースで扱うよりも、ミスを早い段階で見つけやすいのが利点です。

IDE支援を受けやすい

Kotlin DSLはKotlinコードなので、

  • 補完
  • 定義ジャンプ
  • リネーム
  • 型推論
  • エラー表示

などのIDE支援を受けやすいです。

これは特にGradle Kotlin DSLで大きなメリットになります。

宣言的に書きやすい

Kotlin DSLは、処理手順を細かく書くというより、最終的にどのような構造・設定・状態にしたいかを書くのに向いています。

このため、

  • 設定
  • HTMLのようなツリー構造
  • フォーム定義
  • UI定義
  • テスト記述

などと相性がよいです。

Kotlin DSLがよく使われる場面

Gradleのビルド設定

もっとも有名なのがGradle Kotlin DSLです。

Gradleには主に次の2つの記法があります。

  • Groovy DSL
  • Kotlin DSL

Kotlin DSLでは、ファイル名が .kts になります。

  • build.gradle.kts
  • settings.gradle.kts

たとえば依存関係の記述は次のようになります。

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

また、プラグイン指定は次のように書けます。

plugins {
    kotlin("jvm") version "1.9.0"
    application
}

ただし、ここでのバージョン番号はあくまで一例です。

実務では、使っているGradleやKotlinのバージョンに合わせて確認が必要です。

Gradle Kotlin DSLの大きな利点は、

  • 型安全
  • 補完の効きやすさ
  • リファクタリングのしやすさ
  • Kotlin経験を活かしやすいこと

です。

一方で、既存の情報や古い記事ではGroovy DSLの例が多く、最初は読み替えに慣れが必要なこともあります。

HTMLのような構造記述

Kotlin DSLは、階層構造を表現するのに向いています。

代表例として、HTML風のDSLがあります。

html {
    head {
        title {
            +"My Page"
        }
    }
    body {
        h1 {
            +"Hello"
        }
        p {
            +"This is Kotlin DSL."
        }
    }
}

このような書き方は、文字列連結よりも構造が明確で、誤りも見つけやすくなります。

なお、ここで使われている +"text" のような表現は、Kotlin標準でどこでもそのまま使えるわけではありません。

DSL側で operator fun String.unaryPlus() のような演算子関数を定義しているから使える表現です。

テストDSLや宣言的な記述

Kotlinでは、ライブラリやフレームワークの設計次第で、テストやUIを読みやすいDSL風に記述できます。

たとえば、

describe("Calculator") {
    it("adds two numbers") {
        val result = 1 + 2
        assert(result == 3)
    }
}

のような書き方は、仕様書に近い読みやすさを持たせやすいです。

ただし、こうしたDSLはKotlin標準機能そのものというより、各ライブラリがKotlinの機能を使って提供しているものです。

そのため、「Kotlin自体が標準でこれらのDSLを持っている」と理解するのではなく、KotlinはDSLを作りやすい言語であると捉えるのが正確です。

Kotlin DSLは「別言語」ではない

ここは重要です。

Kotlin DSLは、SQLのような外部DSLとは違い、Kotlinの中で書く内部DSLです。

つまり、

  • Kotlinコンパイラでそのまま扱える
  • 通常のKotlinコードとして型チェックされる
  • IDE支援を受けられる

という特徴があります。

このため、Kotlin DSLは見た目が専用言語っぽくても、本質的にはKotlinの言語機能を活用した表現方法です。

Kotlin DSLとBuilderパターンの関係

Kotlin DSLは、Builderパターンと非常に相性がよく、type-safe builder として実装されることが多いです。

たとえばBuilderパターンでは、

val person = PersonBuilder()
    .name("Taro")
    .age(20)
    .build()

のように書くことがあります。

一方、Kotlin DSLでは、

val person = person {
    name = "Taro"
    age = 20
}

のように、より宣言的に書けます。

ただし、Kotlin DSLを単純に「Builderパターンの進化版」とだけ言い切るのは少し狭いです。

なぜならKotlin DSLは、オブジェクト構築だけでなく、

  • 設定
  • 構造記述
  • ルール記述
  • 宣言的な記法

などにも使われるからです。

したがって、より正確には、

Kotlin DSLはBuilderパターンと親和性が高く、type-safe builderとして実装されることが多い

と理解するのが適切です。

@DslMarker の役割

DSLがネストしてくると、内側のブロックから外側のレシーバに触れてしまい、スコープが混ざって分かりにくくなることがあります。

それを防ぐのが @DslMarker です。

@DslMarker
annotation class HtmlTagMarker

このアノテーションをDSLのクラスに付けることで、誤って別のスコープのメンバーを触るのを防ぎやすくなります。

複雑なDSLでは、かなり重要な仕組みです。

infix関数について

Kotlin DSLでは、infix関数を使って自然な読み方に近づけることがあります。

たとえば、

infix fun String.eq(value: String): Pair<String, String> = Pair(this, value)

のように定義すれば、

val condition = "name" eq "Taro"

のように書けます。

ただし、infix表現は多用しすぎると逆に読みづらくなります。

また、to のように標準ライブラリで広く使われる名前を例に出すと、自作定義と標準機能の区別がつきにくくなることがあります。

そのため、DSL設計では自然に見えることより、誤解されにくいことのほうが大切です。

Kotlin DSLの簡単な自作例

最小限のDSLは、次のように作れます。

class Person {
    var name: String = ""
    var age: Int = 0
}

fun person(block: Person.() -> Unit): Person {
    val p = Person()
    p.block()
    return p
}

使い方はこうです。

val p = person {
    name = "Taro"
    age = 20
}

この形がKotlin DSLの基本パターンです。

  1. 設定対象のクラスを作る
  2. ClassName.() -> Unit を受け取る関数を作る
  3. インスタンスを生成してブロックを実行する

この仕組みをベースにして、より複雑なDSLへ発展させていきます。

Kotlin DSL設計で大事なこと

書きやすさより、読みやすさを優先する

DSLは見た目を凝りすぎると、書く人は気持ちよくても読む人には分かりにくくなります。

DSLはチームで読むことが多いため、後から見て意図が分かるかが重要です。

自然言語風にしすぎない

たとえば自然言語風にしすぎると、一見かっこよくても保守しにくくなる場合があります。

user named "Taro" aged 20 livingIn "Osaka"

このような記法は短期的には面白く見えても、定義位置やスコープ、補完性、エラーの分かりやすさの面で不利になることがあります。

スコープを明確にする

DSLではネストが増えるほど、「今どのオブジェクトに対して書いているのか」が分かりにくくなります。

そのため、

  • @DslMarker
  • 適切な関数名
  • 深すぎないネスト
  • 必要に応じた明示的な this

が重要になります。

エラーの分かりやすさも考える

DSL利用者は内部実装を知らないことが多いため、使い方を間違えたときに、何が悪いか分かる設計が大切です。

DSLは「書ける」だけでなく、誤った使い方をしたときにも理解しやすいことが重要です。

必要以上にDSL化しない

単純な処理なら、普通の関数やデータクラスで十分なことも多いです。

DSLは強力ですが、常にDSL化すればよいわけではありません。

DSLにすることで、本当に読みやすく・安全に・保守しやすくなるかを見極めることが大切です。

Kotlin DSLを学ぶときのおすすめ順

Kotlin DSLを学ぶときは、次の順番が理解しやすいです。

  1. Kotlinの基本的なクラスと関数
  2. ラムダ式
  3. 拡張関数
  4. レシーバ付きラムダ
  5. 小さなDSLを自作する
  6. @DslMarker を理解する

特に、まずは既存のDSLを使う側として慣れるのがおすすめです。

  • Gradle Kotlin DSL
  • 小さな設定DSL
  • HTML風DSL
  • テストDSL

などを読むことで、DSLの書き味と設計の考え方が見えてきます。

学習時につまずきやすいポイント

T.() -> Unit が分かりにくい

最初の壁はここです。

ただ実態は、ある型を this として使えるラムダという理解で十分です。

どこから関数が見えているのか分かりにくい

DSL内部ではレシーバのメンバーや拡張関数が自然に見えるため、初見では「この関数はどこから来たのか」が分かりづらいことがあります。

ネストが深いとスコープが分からなくなる

DSLでは入れ子が深くなるほど、どの this を見ているのか分かりづらくなります。

ここで @DslMarker や明示的なスコープ指定が役立ちます。

作るのは簡単でも、良い設計は難しい

文法っぽく見せるだけなら比較的簡単ですが、本当に使いやすいDSLにするには、API設計の力が必要です。

まとめ

Kotlin DSLとは、Kotlinの言語機能を使って、特定の目的に合った読みやすい内部DSLを作る方法です。

特に重要なのは、次の3つです。

  • レシーバ付きラムダ
  • 拡張関数
  • 型安全なビルダー

そして、複雑なDSLでは

  • @DslMarker
  • スコープ設計
  • 読みやすさ重視のAPI設計

も大切になります。

実務では、Gradle Kotlin DSLのように設定を型安全かつ読みやすく書きたい場面で大きな価値があります。

一言でまとめるなら、Kotlin DSLは

Kotlinを使って、特定用途に特化した読みやすい記述スタイルを実現する仕組み

です。

以上、Kotlin DSLについてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次