msgpack4zというmsgpackのScala用ライブラリを作った

twitter上では何度かtweetしてたし、この前msgpackのjavaに関して書いたりしたけど

msgpack javaについて

やっと、ここ2ヶ月くらいの成果を一旦リリースしました。最初はそれほど色々作る予定なかったのに、気がついたらかなり色々できてしまいました。それらの概要や経緯を大雑把に解説します。


いつものように、これ書いてる2015-01-06時点のものなので、細かい部分は今後変わる可能性があります。


まず名前の由来ですが、最初適当にmsgpack4sとかにしようかと思ったけど、ぐぐったら一応その名前で既に存在したので、仕方なくhttpzのときと同じように、sではなくzつけてみただけです。あまり深い意味はありません。


さて、そしてまず

  • とにかく色々できてしまったので、今回はリポジトリ自体を分けた
  • リポジトリ自体が大量になったので、専用のorganization作ってみた

というわけで、以下のところに色々置いてあります。全部ひとまずversion 0.1.0でリリースしました。
https://github.com/msgpack4z

まずは、なぜこんなにリポジトリというかライブラリが大量になってるのか?というのと、それぞれの説明をします。


最初にそれぞれの説明の前の注意事項としては、Scala2.10も切り捨てて、現状Scala2.11だけです。頑張ればScala2.10サポート不可能ではなかった気もしますが、そこは頑張らなくてもいいか、という気分だったので切り捨てました。



さて、まずは依存関係図*1





実際使うにあたっては、上記の依存関係図の理解がかなり重要です。
現状ですでに10個くらいあるのですが、ライブラリの性質として、いくつかのグループに分けることができます

  • jsonライブラリとの相互変換
    • msgpack4z-argonaut
    • msgpack4z-jawn
    • msgpack4z-play
  • 実際に、シリアライズ、デシリアライズを行う低レイヤー(?)な実装部分
    • msgpack4z-java07
    • msgpack4z-java06
    • msgpack4z-native
  • その他
    • その他が全部同じような性質なわけではなく、上記の2グループのものはまとめられるが、それ以外はまとめられない、というだけ


まず、歴史的なことを説明すると

  • msgpackのScalaのライブラリを作ろうと思い立つ(現状のmsgpack-scala使いたくないので)
  • msgpackのjavaのやつ、version 0.7系は速いらしいし、依存ライブラリもゼロだし、version 0.7系に直接依存させて作ってみるか
  • 細かいバグを発見して、色々不安になる
  • msgpack-javaの0.6系も試してみる
  • 0.6系は、それはそれで新しい仕様に完全対応しなかったりする
  • 「難しくなさそうだし、いっそのこと、勉強のためにも自作してみるか」と思いたち、全部自作してみる
  • せっかく色々試したことだし、全部を抽象的に扱えるレイヤーを作って、3種類を切り替えられるようにしてみよう!

という流れで、色々できてしまいました。

ある程度のJavaライブラリでは、そこまで珍しくないとも思うのですが、つまりslf4jとかがたぶんそんなアーキテクチャでしょうか?具体的には、msgpack4z-apiというのが、例えば以下のように最低限のinterfaceだけが定義されています

https://github.com/msgpack4z/msgpack4z-api/blob/v0.1.0/src/main/java/msgpack4z/MsgPacker.java

public interface MsgPacker {

    void packByte(byte a) throws IOException;
    void packShort(short a) throws IOException;
    void packInt(int a) throws IOException;
    void packLong(long a) throws IOException;
    void packDouble(double a) throws IOException;
    void packFloat(float a) throws IOException;
    void packBigInteger(BigInteger a) throws IOException;
    void packArrayHeader(int a) throws IOException;
    void arrayEnd() throws IOException;
    void packMapHeader(int a) throws IOException;
    void mapEnd() throws IOException;
    void packBoolean(boolean a) throws IOException;
    void packNil() throws IOException;
    void packString(String a) throws IOException;
    void packBinary(byte[] a) throws IOException;
    byte[] result() throws IOException;

}

上記は、シリアライズの方のinterfaceですが、デシリアライズも同じような感じです。
とくに深い理由もないですが、Scalaである必要が特になかったので、msgpack4z-api、msgpack4z-java06、msgpack4z-java07、の3つはpure javaで書かれています。一応依存少なくなるとか、後々Scalaのバイナリ互換で悩まされずに済む、という利点はありますね。


そもそもhttpzも、ある程度似たようなアーキテクチャで作りました。つまり
「実際の実行する低レイヤー部分は、既存のライブラリを利用しつつ、それらを抽象化しておいて、切り替えられるようにする」
というアーキテクチャです。

これはこれで、それほど必要ないのにあまりここまで頑張ってもわかりずらかったりメンテしずらいデメリットもあるとは思います。例のごとく、勉強のためというか、実際色々やってみて、どういうデメリット、メリットがあるのか体感してみるため、というのが大きいので、とにかくこの方針でやってみることにします。

あと、こうしておくと、パフォーマンスの比較がやりやすくなると思うので、あとでやりたい(まだやってない・・・)


そして、msgpack4z-apiさえ変更しなければ、それぞれのモジュールに変更を加えても、全部をリリースし直す必要がなくなります。
それをやることを想定しているので、わざわざリポジトリ自体を分けました。
*2
逆に言うともしmsgpack4z-apiに対して非互換の変更をしたら、全部をアップデートしないといけなくなりますが、それを頻繁にやってしまうなら分けた意味が半減するというか、リポジトリ分けたことによるデメリットのほうが大きくなってしまうので、実際よほどのことが無い限り、数ヶ月から1年のレベルでmsgpack4z-apiは変更する予定はありません。




少し無理やり最大公約数的な定義にしたことにより、微妙にデメリットも生じています。例えば

  • msgpack-java06系は、一部のメソッドなどをリフレクション経由で呼び出し
  • msgpack-java06系に合わせるために、msgpack07では必要ない、arrayEndやmapEndというメソッドを作らざるを得なかった
  • 色々な入力に対応するの面倒だったので、デリシリアライズは、ひとまずArray[Byte]からのみ

などです。上記に挙げたくらいで、そこまで多くないので、一応上記のデメリットあっても、メリットも結構あって実用的では?、という判断のもとにやってます。




さて、次はcore部分の説明をします。まず、core部分はScalaz7.1に依存してます。
ここは迷ったのですが、標準のEither使いたくないなどの理由により依存させてしまいました。

coreの部分は、まず型クラスベースで作りました。もともと、msgpackのScalaライブラリを作るにあたって、型クラスを使っての定義が一番やりたかったことですね。
型クラスの定義の中心部分は、以下のようなものです

https://github.com/msgpack4z/msgpack4z-core/blob/v0.1.0/src/main/scala/msgpack4z/MsgpackCodec.scala

trait MsgpackCodec[A] {

  def pack(packer: MsgPacker, a: A): Unit

  def unpack(unpacker: MsgUnpacker): UnpackResult[A]

}

MsgpackCodecというのが唯一の型クラスです。
先ほどあまり詳しく説明していませんでしたが、MsgPackerとMsgUnpackerは、msgpack-apiに定義されてるinterfaceです。
packがシリアライズ、unpackがデシリアライズのメソッドです。
ここで、あえてシリアライズとデシリアライズの型クラスを分けずに、必ず両方を定義しないといけない仕組みにしました。
理由としては

  • そのほうがmsgpack-core側の実装が少し楽できるというか、シンプルになる
  • 「必ず両方を定義しないといけない」という手間をユーザー側に強制しても、慣れればそこまで面倒でもない
  • 結局テストをする場合に両方必要になることがしばしばある(Jsonで似たようなことをした経験上)
  • シリアライズやデシリアライズ方法は色々ありえて、一意に定まるとは限らないので、もしシリアライズとデシリアライズするプログラムが同じなら、必ず同時に両方定義することを強制してしまったほうが、ミスが少なくわかりやすくなる

などです。

また、packのシグネチャをみてもらうと、戻り値型がUnitということから察しがつくように、packは純粋な操作ではなく、副作用があります。MsgPackerの状態を変更します。
そもそも、これも説明してなかったですが、MsgPackerもMsgUnpackerもほぼ全部のメソッドに副作用があります。

もし純粋関数型な感じにするとしたら、おそらく、もみあげさんが作ってる以下のライブラリ

https://github.com/pocketberserker/scodec-msgpack

のようなアプローチになると思うのですが、

  • データ型それぞれ作るの面倒
  • 中間データ発生して効率よくなさそう(計測したわけではない)
  • msgpackのフォーマットの性質上、先頭から順番に読んでいって、シリアライズ、デシリアライズするのがとてもやりやすいので、上記のようなmutableなインターフェイスにしても、型クラスのインスタンス定義側はあまり苦にならない
  • こうなっていても、型クラスのインスタンスを使用する側からは、ほぼimmutableに便利に扱える

などの理由により、こうなりました。


この型クラス部分の特徴としては、

  • List, Map, Vector, Option, Array, Either, Tuple, case classなどの、よく使うもののデフォルトのインスタンス定義
  • case classやTupleに関しては、コードの自動生成してるので、もちろん22まで対応
  • AnyValのArrayに関しては、それぞれ専用に実装してるので、boxingなどが発生しないように配慮していて、効率がよい(はず)
  • Scalazに依存させた都合上、ScalazのMaybe, IList, NonEmptyList, IMapなどのインスタンスも定義

などです。ここは自信がある、といかこのために作ったと言っても過言ではないので、明らかにこうやって型クラスベースにしたほうがmsgpack-scalaより型安全で使いやすいと思います。
また、msgpack-scalaはcase classに対応してない!?らしいですが、case classのインスタンスに関しても、かなり最小限の手間で定義できるようになってます。



また、先ほど説明したように、msgpack4z-coreが依存してるのはmsgpack4z-apiインターフェイスの部分のみなので、実際に使ってシリアライズ、デシリアライズをする場合には、msgpack4z-java07、msgpack4z-java06、msgpack4z-native、の3つのうちどれかを組み合わせないと動かせません。


その他、細かい実装詳細で話したいことは色々あるのですが、一旦このくらいにして、ほかのモジュールの説明にいきます。


msgpack-shapelessは、以前説明した原理
代数的データ型とshapelessのマクロによる型クラスのインスタンスの自動導出
を使って、case classのインスタンスを完全自動生成するものです。
使用例は、このあたりのテストコード
https://github.com/msgpack4z/msgpack4z-shapeless/blob/v0.1.0/src/test/scala/msgpack4z/AutoTest.scala
見てください。



残りのJson関連ライブラリの3つは説明疲れてきたというか、それほど説明することないので雑に済ませますが、それぞれのJsonのclassから、Jsonの文字列を経由せずに直接msgpackのByte列に変換可能なものです。
以下のようなJsonとmsgpackの微妙な差異

  • msgpackはkeyにString以外を指定することが可能
  • msgpackの数値には、NaNやInfinityがある(Jsonには無い)
  • play-jsonにはundefinedという謎のオブジェクトが・・・

があるので、それらの対応出来ない場合の挙動をオプションで指定できるようにしておいたあたりが工夫した点でしょうか。

以下、play-jsonでの例
https://github.com/msgpack4z/msgpack4z-play/blob/v0.1.0/src/main/scala/msgpack4z/PlayUnpackOptions.scala


play, argonaut, jawn以外の、sprayとかjson4sとかがない理由は、特にありません。単に直近で自分が使う予定なさそうで、作るの面倒だっただけです。気が向いたら作るかもしれないし、作らないかもしれません。




思ったよりは説明長くならなかったですが、とりあえずこんなところにしておきます。今後どのくらい真面目にメンテするのか実用するのかはわからないですが、なにかあればフィードバックお待ちしてます。


あと、msgpack-javaのValueのAPIに相当するものも、一から自作してるので、うまくいったらそのうちリリースするかもしれません。
version0.1.1でリリースしました。それを使うことにより、0.1.1から、case classをmapにもarrayにもシリアライズ、デシリアライズできるようになりました。

*1:依存ライブラリ自体が依存してるライブラリは省略してあります

*2:リポジトリ同じでも、一部のモジュールだけリリースすることは不可能ではないですが、git上のtagの付け方などがややこしくなってやりずらいので