読者です 読者をやめる 読者になる 読者になる

算譜王におれはなる!!!!

偏りはあると思うけど情報技術全般についてマイペースに書くよ。

KotlinでGroovyスタイルの型安全ビルダ

※下記URLのサイトを参考にしました。英語、または技術的な知識が至らず、内容に誤りが含まれるおそれがありますので、ご了承ください。
※本エントリは参考サイトの翻訳をベースに、加筆・変更を施した構成となっています。
※サンプルコードは参考サイトから引用しています。

参考サイト http://confluence.jetbrains.net/display/Kotlin/Type-safe+Groovy-style+builders

ビルダ(builder)の概念は、むしろGroovyコミュニティで好評です。ビルダは半宣言的な方法でのデータ定義を可能にします。ビルダはXMLの生成UIコンポーネントのレイアウト3Dシーンの描画などに効果覿面です。

多くのユースケースに対して、Kotlinはビルダの型チェックができ、Groovyで作られた動的型付け実装よりもビルダを一層魅力的にします。

残りのケースに対して、Kotlinは動的なビルダをサポートします。

型安全なビルダの例

ここから取ってきて、少し改造した次のコードについて考えましょう。

import html.*

fun result(args : Array<String>) =
  html {
    head {
      title {+"XML encoding with Kotlin"}
    }
    body {
      h1 {+"XML encoding with Kotlin"}
      p {+"this format can be used as an alternative markup to XML"}

      // an element with attributes and text content
      a(href = "http://jetbrains.com/kotlin") {+"Kotlin"}

      // mixed content
      p {
        +"This is some"
        b {+"mixed"}
        +"text. For more see the"
        a(href = "http://jetbrains.com/kotlin") {+"Kotlin"}
        +"project"
      }
      p {+"some text"}

      // content generated by
      p {
        for (arg in args)
          +arg
      }
    }
  }

これは完璧に合法なKotlinコードです。

どのように動作するのか

Kotlinの型安全ビルダの実装の仕組みをウォークスルーしましょう。まず第一に、ビルドしたいモデルを定義する必要があります。このケースでは、HTMLタグのモデル作りが必要となります。それはクラス群を用いることで簡単に行えます。例えば、HTMLはタグを表すクラス、すなわちそれはやのような子を定義します。

では、何故コード中でこのようなことができるのか思い出してみましょう。

html {
 // ...
}

これは、引数として関数リテラルを取る関数呼び出しです(詳細はこのエントリを参照)。実際にこの関数は次のように定義されています。

fun html(init : HTML.() -> Unit) : HTML {
  val html = HTML()
  html.init()
  return html
}

この関数は、それ自身が関数であるinitと名付けられたパラメータをひとつ取ります。実際にはHTML型をレシーバとする(そして、面白いものは何も返さない、すわなちUnitを返す)拡張関数です。したがって、htmlに引数として関数リテラルを渡したとき、それは拡張関数として型付けされ、そこでthis参照が有効になります。

html {
  this.head { /* ... */ }
  this.body { /* ... */ }
}

(headとbodyはHTMLのメンバ関数です。)
ここで、thisはいつものように省略可能です。そして、早くもビルダにそっくりに見えるものを手に入れました。

html {
  head { /* ... */ }
  body { /* ... */ }
}

この呼び出しは何を行うのでしょうか?先に定義したhtml関数の本体を見てください。それはHTMLの新しいインスタンスを生成し、引数として渡された関数を呼び出して初期化し(この例では結局、HTMLインスタンスでbodyを呼び出しているということです)、そしてこのインスタンスを返します。これはまさにビルダがすべきことです。

HTMLクラスのheadとbody関数は、htmlと同様に定義されています。唯一の違いは、HTMLインスタンスのエンクロージングであるchildrenコレクションにビルドされたインスタンスを追加することです。

fun head(init : Head.() -> Unit) {
  val head = Head()
  head.init()
  children.add(head)
  return head
}

fun body(init : Body.() -> Unit) {
  val body = Body()
  body.init()
  children.add(body)
  return body
}

実際には、この2つの関数はまったく同じことを行います。なので一般的なバージョンであるinitTagを使うこともできます。

protected fun initTag<T : Element>(init : T.() -> Unit) : T
    where class object T : Factory<T> {
  val tag = T.create()
  tag.init()
  children.add(tag)
  return tag
}

この関数はクラスをインスタンス化するのにクラスオブジェクトを使用します。それは次のように定義されるFactoryクラスに依存します。

abstract class Factory<T> {
  fun create() : T
}

HeadとBodyクラスはFactoryクラスを拡張するクラスオブジェクトを宣言しています。例えば

class Head() : TagWithText("head") {
  class object : Factory<Head> {
    override fun create() = Head()
  }

  // ...
}

そのため、関数はすごくシンプルです。

fun head(init : Head.() -> Unit) = initTag(init)

fun body(init : Body.() -> Unit) = initTag(init)

そして私たちはとタグをビルドするのに、これら使用することができます。

ここで議論されるもうひとつのことは、タグボディにテキストを追加する方法です。上記の例では次のように書いています。

html {
  head {
    title {+"XML encoding with Kotlin"}
  }
  // ...
}

基本的にはタグ本体の内側に文字列を置きますが、その前に「+」があります。それはplus演算子呼び出しです。この演算子は実際には、(Titleの親である)TagWithText抽象クラスのメンバであるplus拡張関数によって定義されています。

fun String.plus() {
  children.add(TextElement(this))
}

したがって「+」接頭辞がここで行うことは、その文字列が適切なタグツリーの一部となるように、TextElementのインスタンスの中へ文字列をラップし、それをchildrenコレクションに追加することです。

これらは全て、上記のビルダの例のトップでインポートされているhtmlパッケージで定義されています。次のセクションでは、このパッケージの完全な定義を読み切ることができます。

htmlパッケージの完全な定義

htmlパッケージがどのように定義されているのか(上記の例で使用されている要素のみ)示すセクションです。それはHTMLツリーをビルドします。それは拡張関数と拡張関数リテラルを多用しています。

注意:次のコードの1行目は、引用元では namespace html { となっていますが、namespace名前空間を定義するためのキーワードの古い仕様です。現在はpackageを使わなければなりません。そのため、ここではnamespacepackageに置換しています。

package html {

  abstract class Factory<T> {
    fun create() : T
  }

  abstract class Element

  class TextElement(val text : String) : Element

  abstract class Tag(val name : String) : Element {
    val children = ArrayList<Element>()
    val attributes = HashMap<String, String>()

    protected fun initTag<T : Element>(init : T.() -> Unit) : T
      where class object T : Factory<T> {
      val tag = T.create()
      tag.init()
      children.add(tag)
      return tag
    }
  }

  abstract class TagWithText(name : String) : Tag(name) {
    fun String.plus() {
      children.add(TextElement(this))
    }
  }

  class HTML() : TagWithText("html") {
    class object : Factory<HTML> {
      override fun create() = HTML()
    }

    fun head(init : Head.() -> Unit) = initTag(init)

    fun body(init : Body.() -> Unit) = initTag(init)
  }

  class Head() : TagWithText("head") {
    class object : Factory<Head> {
      override fun create() = Head()
    }

    fun title(init : Title.() -> Unit) = initTag(init)
  }

  class Title() : TagWithText("title")

  abstract class BodyTag(name : String) : TagWithText(name) {
  }

  class Body() : BodyTag("body") {
    class object : Factory<Body> {
      override fun create() = Body()
    }

    fun b(init : B.() -> Unit) = initTag(init)
    fun p(init : P.() -> Unit) = initTag(init)
    fun h1(init : H1.() -> Unit) = initTag(init)
    fun a(href : String, init : A.() -> Unit) {
      val a = initTag(init)
      a.href = href
    }
  }

  class B() : BodyTag("b")
  class P() : BodyTag("p")
  class H1() : BodyTag("h1")
  class A() : BodyTag("a") {
    var href : String
      get() = attributes["href"]
      set(value) { attributes["href"] = value }
  }

  fun html(init : HTML.() -> Unit) : HTML {
    val html = HTML()
    html.init()
    return html
  }

}

おまけ:Javaクラスをよりよくする

上記コード中に、ベリーナイスなものがあります:

class A() : BodyTag("a") {
    var href : String
      get() = attributes["href"]
      set(value) { attributes["href"] = value }
  }

[]演算子だけで、まるで連想配列であるかのようにattributesマップにアクセスできます。規則によって、これはget(K)またはset(K, V)の呼び出しへとコンパイルされます。しかし、attributesはJavaのMapです。つまり、それはset(K, V)を持ちません。この問題はKotlinでは簡単に修復可能です:

fun <K, V> Map<K, V>.set(key : K, value : V) = this.put(key, value)

そう、単純に拡張関数set(K, V)を定義します。それは陳腐なputへ委譲し、JavaクラスでKotlinの演算子を利用可能にします。