KotlinでJSON構築DSLを作る

  • 14
    いいね
  • 1
    コメント
この記事は最終更新日から1年以上が経過しています。

この記事はKotlin Advent Calendar 2014の7日目の記事です。
昨日は@peko_kunさんによるKotlinでWebをやってみる - Hello Node.ktでした。

Kotlinとは

Kotlin (コトリン)とは、IntelliJ IDEAなどのIDEで有名なJetBrains社が中心となって開発が進められているプログラミング言語です。JVM言語のひとつで、コンパイラはJavaバイトコードを出力します。いわゆるBetter Javaという位置づけで、Javaから離れすぎないシンプルな文法が特徴的ですが、Javaの悪しき習慣を招いたり、後方互換を維持するために残っているような文法・機能を削除し、Javaにはない強力な機能を有した言語です。JetBrains社はJavaとの完全な相互運用性と、Javaと同等以上のコンパイル速度、実行速度を目指すと言っています。
Kotlinに興味を持たれた方はすぐに試すことができます。Kotlin Web Demoはお使いのWebブラウザ上でKotlinコードを編集、実行ができます。シンタックスハイライトやメソッド名補完などを行ってくれる便利なツールです。

今回のゴール

今回はKotlinを使ってJSONを構築するためのDSL(内部DSL)を作ってみたいと思います。
具体的には次のJSONを出力したいです。

{
  "articles": [
    {
      "title": "Foo",
      "tags": [1, 2],
      "author": { "id":"111", "name":"hoge" }
    },
    {
      "title": "Bar",
      "tags": [3],
      "author": { "id":"222", "name":"fuga" }
    }
  ]
}

そしてこのJSONに対応するKotlinコードを下記のような記述で実現したいです。

jsonObject {
  "articles" to jsonArray {
    +jsonObject {
      "title" to "Foo"
      "tags" to listOf(1, 2)
      "author" to jsonObject {
        "id" to 111
        "name" to "hoge"
      }
    }
    +jsonObject {
      "title" to "Bar"
      "tags" to listOf(3)
      "author" to jsonObject {
        "id" to 222
        "name" to "fuga"
      }
    }
  }
}

使用する言語機能

今回のDSLを作るにあたって使用するKotlinの言語機能のうち、特徴的なものを紹介します。

中置呼び出し

中置呼び出しという構文糖衣があります。次の2つのメソッド呼び出しは等価です。

"Kotlin".compareTo("Java")
"Kotlin" compareTo "Java"

後者では、ドットと引数の括弧が省略されています。 「レシーバ、メソッド、引数1つ」となるようなメソッド呼び出しの場合に限り中置呼び出しによる記述が可能です。

拡張関数

拡張関数は既存のクラスにメソッドを追加できる機能、構文です。Stringクラスにshowメソッド追加する例を示します。

fun String.show() {
  println(this)
}

通常の関数、メソッドのようにfunキーワードを伴って定義します。この拡張関数がスコープにある箇所でshowStringのメソッドのように扱えます。

"Merry Christmas".show() // => Merry Christmas

演算子オーバロード

Kotlinは演算子オーバロードをサポートしています。複雑さを避けるためかGroovyのように、演算子に対応した名前を持つメソッドを定義することで演算子オーバロードを実現しています。2項演算子の+は、ひとつの引数を取るplusメソッドに対応する、という具合です。

高階関数とラムダ式

Kotlinでは関数オブジェクトを引数として取る高階関数が多く登場します。大抵の場合は、関数オブジェクトをその場で定義するラムダ式[1] が便利です。関数リテラルは必ず{}が必要です。

listOf(1, 2, 3).first({
  i -> i % 2 == 0
})

上記の例はリストからラムダ式で渡した条件を満たす最初の要素を取得するメソッドfirstを使っています。このようにメソッド(あるいは関数)の引数にラムダ式を渡す場面が多数あるので構文糖衣が用意されています。

listOf(1, 2, 3) first {
  i -> i % 2 == 0
}

引数リストの最後が関数を取る場合、ラムダ式を引数リストの括弧の外に追い出して記述できます。さらに上記の例ではfirstを中置呼び出ししており、単なる標準ライブラリの関数呼び出しが、まるで組み込み構文のように見えます。

拡張関数とラムダ式

言葉で説明するよりコードで説明した方がわかりやすそうなので、まずコードから。

fun createJFrame(setup: JFrame.()->Unit): JFrame {
  val jframe = JFrame()
  jframe.setup()
  return jframe
}

createJFrameという関数を定義しました。setupは引数の名前です。Kotlinは変数の後に型を置くのでJFrame.()->Unitsetupの型です。引数の型リスト->返値の型は関数の型です。例えば(String, Int)->Userは、StringIntを引数にとってUserを返す関数と読めます。setupUnitを返す関数ですが、Unitは意味ある値を持たない型でJavaでいうvoidと同じです。
型が()->Unitだけならば「引数なしで値を返さない関数」と解釈できますが、ここではJFrame.()->Unitとなっています。この型は「引数なしで値を返さないJFrameのメソッド」です。どういうことなのか。。createJFrameの使用例を示します。

val jframe = createJFrame {
  this.setTitle("My Swing App")
  this.setSize(200, 200)
  this.setDefaultCloseOperation(EXIT_ON_CLOSE)
}

createJFrameの引数にラムダ式を渡しているのがわかると思います。そのラムダ式の中でthisをレシーバとしてsetTitlesetSizeなどのメソッドを呼び出しています。もうおわかりだと思いますが、このthis、実はJFrameインスタンスです。もちろんthisを省略してsetTitle("My Swing App")と記述することも可能です。

実装

道具が揃いました。実装は非常にシンプルです。
全部作るのは面倒だし、この記事にとってはあまり意味がないので今回はorg.jsonを使います。

fun jsonObject(setup: JsonObjectBuilder.() -> Unit): JSONObject
        = JsonObjectBuilder().let { it.setup(); it.jsonObject }

fun jsonArray(setup: JsonArrayBuilder.() -> Unit): JSONArray
        = JsonArrayBuilder().let { it.setup(); it.jsonArray }

class JsonObjectBuilder {
    val jsonObject = JSONObject()

    fun <T> String.to(value: T) {
        jsonObject.put(this, value)
    }
}

class JsonArrayBuilder {
    val jsonArray = JSONArray()

    fun <T> T.plus() {
        jsonArray.put(this)
    }
}

これで全部です。よく見ると紹介した機能が使われていることがわかります。
少し補足すると、JsonObjectBuilderString.toJsonArrayBuilderT.plusという拡張関数をクラスのメンバとして持っています。これにより、拡張関数のスコープが絞れます。具体的にはそれを定義したクラス内でのみ有効になります(でもprivateではない)。

jsonObject {
  // この中はJsonObjectBuilderの拡張関数としてのラムダ式
  // ここの to はJsonObjectBuilderで定義した拡張関数String.toを指す
  "key" to "value"
}

実装が完了した今、欲しかったJSONをKotlinで表現できるようになりました。

// Kotlinプログラムのエントリポイント
fun main(args: Array<String>) {
  val json: JSONObject = jsonObject {
    "articles" to jsonArray {
      +jsonObject {
        "title" to "Foo"
        "tags" to listOf(1, 2)
        "author" to jsonObject {
          "id" to 111
          "name" to "hoge"
        }
      }
      +jsonObject {
        "title" to "Bar"
        "tags" to listOf(3)
        "author" to jsonObject {
          "id" to 222
          "name" to "fuga"
        }
      }
    }
  }

  println(json) // => 欲しかったJSONが出力される
}

最後に

Kotlin、かわいい名前で簡単に始められそうな雰囲気ですが、強力な機能を持ち表現力豊かな言語です。
この記事で紹介していない機能はまだまだあるので、興味のある方はぜひ公式ドキュメント、または私のブログをご覧ください。
明日は@yy_yankさん(今季3回目)のポエム的なやつです。

注釈

[1] Kotlinの世界ではラムダ式を関数リテラルと呼ぶことが多いです