プログラマ英語学習日記

プログラミングと英語学習のまとめなど

Kotlin DSL入門

Kotlinはドメイン固有言語(DSL)として作りやすい言語ですが、大半の人は使ったことはあっても作らないのではないでしょうか?こういうものですね

html {
  head {
    title("タイトル")
  }
}

必然性があまりないのは事実ですが、簡単なものを作れるようになっておくと言語の知識が深まります。ここではごく部分的なHTMLを題材にDSLのコードを書いてみましょう。

基本(1) 拡張関数

まず使う言語機構が、拡張関数 です。既存クラスに関数を追加する機能ですね。といってもこちらはメジャーなのでみなさん使われていると思います。

fun Int.isOdd(): Boolean {
  return this % 2 > 0
}

1.isOdd() //varのほうがよかったかも

こちらはこれで終了。おなじみ

基本(2) this指定つき高階関数

もう一個、こちらは使っているとは思いますがあまり作らない機能。ラムダ関数内の this を指定したバージョンです。

Kotlin公式Docsでは Function literals with receiver と表現されてます。日本語に訳すとどうなるんでしょうね?

まず普通の高階関数の例

//普通の高階関数を受け取る例。
//面白い例が出てこず意味のない関数になってしまいました
fun sample(value: Int, block: (Int) -> String): String {
    return block(value)
}
fun main() {
    val o = sample(10) {
        "SAMPLE:${it * it}" //it=10
    }
    println(o) //SAMPLE:100
}

これをちょっと細工します

fun sample(value: Int, block: Int.() -> String): String { //Int.() がミソ
    return block(value)
}
fun main() {
    val o = sample(10) {
        "SAMPLE:${this * this}" //thisがIntになる!
    }
    println(o) //SAMPLE:100
}

こうすると、 sampleで指定したラムダのthisが変わります。thisが 3行目で渡されたInt(=10)になるのです。


これを使っているのがスコープ関数です。apply などですね。

fun main() {
    val a = StringBuilder().apply {
        append(1)
        append("A")
    }
    println(a) //1A
}

ラムダ内のthisは StringBuilder。同じ構文を Android-ViewやBundle でやってる人は多いでしょう。

TextView().apply {
    text = ...
    color = ...
}

意識してないかもしれませんが、この FunctionLiteralWithReceiver はある程度使っているはずです。


とはいえ先程のサンプルではあまり使い勝手がよろしくありません。これをIntの拡張関数にしてみましょう

fun Int.sample(block: Int.() -> String): String {
    return block(this)
}
fun main() {
    val o = 10.sample {
        "SAMPLE:${this * this}"
    }
    println(o)  //SAMPLE:100
}

見た目がすっきりしましたね。この構文がDSLを記述するときのベースになります


DSL基礎構造

まずはよくあるHTMLのKotlin-DSL、だいたい以下のようなコードを書くはずです。(※既存ライブラリに似せた架空構文)

val p: String = html {
  head {
    title {
      "Title"
    }
  }
  body {
    h1 {
      "Body"
    }
  }
}
println(p.toString()) //<html>略</html> としたい

お気づきかと思いますが、htmlブロック内の this は Html などです。ようは thisはこのコードが記述されているクラス自身ではありません。 つまり、内部的に以下のようなものがあるのです。

class HtmlTag() {
    fun toHtmlString(): String {
        return "<html></html>" //他多数
    }
}
fun html(block: HtmlTag.() -> Unit): String {
  val tag = HtmlTag()
  tag.block()
  return tag.toHtmlString()
}
//
fun main() {
    val h = html {
    }
    println(h) //<html></html>
}

この HtmlBuilderに headtitle などを メソッドとして を追加していくのです。これがDSLパターンの一例です。

//headタグのメソッド定義(※HtmlTag内に書いてもいいです)
fun HtmTag.head(block: HtmTag.()) {
}

//こう書けるようになる。
val h = html {
  head {
  }
}

作り込み

ではここから各要素を作成していきます。の前に、親子構造が必要なのでBuilderだけではつらい。またタグ名もまったく考慮してません。そのためさきにデータ構造を考慮し少し修正を加えます。

class HtmlTag(val tag: String) {
    //子要素
    val children = mutableListOf<HtmlTag>()    
    //タグ内のテキスト。本気でやりたい場合、これではまずい
    var value: String? = null
    //本来はattributesもほしい
    
    fun toHtmlString(): String {
      //省略
    }
}
fun html(block: HtmlTag.() -> Unit): HtmlTag {
      val html = HtmlTag("html")
    html.block()
    return html
}
fun main() {
    val response = html {
    }
    println(response.toHtmlString()) //<html></html>
}

雑な構造ですが Blog ということもありコードを短く収めたいのでご了承ください。

html を呼ぶことでルート要素が生成されます。あとはこの HtmlTag に色々タグ(=メソッド)を追加していくだけです。ためしにheadタグの場合

fun HtmlTag.head(block: HtmlTag.() -> Unit): Unit {
    val head = HtmlTag("head")
    head.block()
    this.children.add(head)
}

こうなります。あとは似たようなことをすればOK...ですがタグ内の単純文字列はちょっと厄介。本来ならもっと丁寧に処理すべきですが、今回は Tag に value を一個もたせて解決させました。

fun HtmlTag.text(text: String): Unit {
    this.value = text
}

ここもサンプルコードを短くしたい、ということでお許しください。


全体像

そして全体としては以下のようになります

class HtmlTag(val tag: String) {
    val children = mutableListOf<HtmlTag>()    
    var value: String? = null
    
    fun toHtmlString(): String {
        var text = "<${tag}>\n"
        value?.let {
            text += "${it}\n"
        }
        children.forEach {
            text += it.toHtmlString()
        }
        text += "<${tag}>\n"
        return text
    }
}
//ルート要素
fun html(block: HtmlTag.() -> Unit): HtmlTag {
    val html = HtmlTag("html")
    html.block()
    return html
}
//子要素。タグごとにクラスを作り、必須要素を引数にするのも面白い
fun HtmlTag.head(block: HtmlTag.() -> Unit): Unit {
    val head = HtmlTag("head")
    head.block()
    this.children.add(head)
}
fun HtmlTag.title(block: HtmlTag.() -> Unit): Unit {
    val head = HtmlTag("title")
    head.block()
    this.children.add(head)
}
fun HtmlTag.body(block: HtmlTag.() -> Unit): Unit {
    val head = HtmlTag("body")
    head.block()
    this.children.add(head)
}
fun HtmlTag.text(text: String): Unit {
    this.value = text
}

//#
fun main() {
    val h = html {
        head {
            title {
                text("テキスト")
            }
        }
        body {
            text("ボディ")
        }
    }.toHtmlString()
    println(h)
}
//出力(一部改行省略)
//<html>
//<head>
//<title>テキスト<title>
//<head>
//<body>BODY<body>
//<html>

ざっと動きました!


まとめ

このようにすることで自作のKotlin-DSLができます。複雑な親子関係がある構造を構築したい場合に有効で、表層のコードがぐっとわかりやすくなります(裏で動いているものは複雑ですが)

あまり活用するパターンはないかもしれませんが、裏側の構造を知っていたり今回取り上げた Function literals with receiver の構文は知っておくと色々役に立つのでしょう。

何より単純なものでもいいので一度自作してみると非常に勉強になります。DSLかけそうかな?と思ったらぜひトライしてみてください