scala-zero-formatterのパフォーマンス改善やakka-httpサポートとか

https://github.com/pocketberserker/scala-zero-formatter/releases/tag/v0.7.0

表題の件以外にもlz4とzstdへの変換をサポートしてみたりしました。 この2つはあくまでとりあえず程度なので速度そんなにでないですが……。

速度改善の話

  • 余分な型クラスを除去
    • 一時期のバージョンでいれていたが今は無くても動く(もしかしたら昔も動いたのかもしれない)
    • ついでに一部パッケージを統合
  • unsafeモジュールでのStringのエンコード方法変更
  • case classの全フィールドのバイナリサイズがわかっている場合はmacroでバイナリサイズを算出する
    • 最後のresize(arrayコピー)が減る

内部でprotobuf-javaのUnsafeを使っているので、そりゃScalaPBと速度で勝負できるよね…という。 とはいえデシリアライズがまだ若干遅いことがある問題?もあるのでなんとかできないか調査中。

akka-http

MarshallerとUnmarshallerをつかえば簡単にできそうだったので作ってみた、という。 JSON系ライブラリはspray-jsonは公式さぽーとだったり、他のライブラリも既にライブラリがあったりします。

msgpack用のライブラリもそのうち誰かがつくるのではないでしょうか?

lz4, zstd

最初に書きましたがまだお試しパッケージです。 そのため思ったほど速度がでないと思います(LZ4のデシリアライズはその特性上十分早いけど)。

あと、バイナリサイズを考慮して使うか決めないと圧縮前よりサイズが増える、なんてこともなくはない?

scala-zero-formatterのパフォーマンス改善と疑似Stage2対応

とりあえず動くものを~ということで0.1.0でリリースしていたのですが、ベンチマークをとったらさすがにJSON系ライブラリよりは速いものの、他のバイナリシリアライザに倍以上差をつけられる結果に「さすがに遅すぎる」と改善することにしました。

この記事を書いた時点では0.3.0が最新版です。

改善内容

  • foldLeft,、forearchからtailrec、while loopに
    • 初期は動くこと優先で高階関数を使っていた
    • が、 http://d.hatena.ne.jp/xuwei/20130709/1373330529 にもあるとおりtailrecやwhileのほうが早いので差し替え
    • 抽象化をそもそも想定していないのでとれる選択(読みづらいけど)
    • ちなみにこれだけでそこそこ速度が改善する
  • mutableなコレクションやnewBuilderの利用
    • var x = Hoge.emptyで変更するよりmutableコレクションから変換したほうが早い(そりゃそうだ)
  • Encoder, Decoderクラスを追加
    • mutableな設計に倒した
    • 余計な処理が少し減る
  • 一部のHListを自前のマクロに置き換え
    • foldLeftは遅い
  • fixed sizeなコレクションは個別にFormatterインスタンスを用意
    • 汎用的なやつだと毎回Array[Byte]のサイズチェックやコピーが挟まるので遅い
    • ので、サイズがわかっているものは最初に一括でリサイズして、配列のサイズチェックなしに書き込む

UnionやEnumには手を入れていないのであれらはまだ遅いままかも。

改善後のベンチマーク

以下てきとーベンチマーク。 てきとーなのであんまりフェアじゃないかも(とくにJSON系)。

https://github.com/pocketberserker/scala-zero-formatter-benchmarks/tree/a82d1ac5b72ac25599f9984bb0dfba5a48dea84d

sbt "jmh:run -i 10 -wi 10 -f 2 -t 1 benchmarks.EncodingBenchmark"

Benchmark                        Mode  Cnt        Score        Error  Units
EncodingBenchmark.encodeBarZ    thrpt   20  1324256.775 ± 82009.746  ops/s
EncodingBenchmark.encodeFooMZ   thrpt   20   518515.724 ±  8547.872  ops/s
EncodingBenchmark.encodeFooPB   thrpt   20  1650635.690 ± 24153.038  ops/s
EncodingBenchmark.encodeFooZ    thrpt   20  1402245.092 ± 69284.956  ops/s
EncodingBenchmark.encodeFoosA   thrpt   20     1356.404 ±    80.054  ops/s
EncodingBenchmark.encodeFoosC   thrpt   20     2157.034 ±   219.598  ops/s
EncodingBenchmark.encodeFoosMZ  thrpt   20    11213.833 ±  1080.450  ops/s
EncodingBenchmark.encodeFoosZ   thrpt   20    15255.728 ±  1111.828  ops/s
EncodingBenchmark.encodeIntsA   thrpt   20    10119.779 ±   894.966  ops/s
EncodingBenchmark.encodeIntsC   thrpt   20    14285.633 ±  1583.352  ops/s
EncodingBenchmark.encodeIntsMZ  thrpt   20    83462.462 ±  1648.580  ops/s
EncodingBenchmark.encodeIntsPB  thrpt   20   151786.501 ±  2536.334  ops/s
EncodingBenchmark.encodeIntsZ   thrpt   20   219506.973 ±  6170.565  ops/s

sbt "jmh:run -i 10 -wi 10 -f 2 -t 1 benchmarks.DecodingBenchmark"

Benchmark                        Mode  Cnt        Score        Error  Units
DecodingBenchmark.decodeBarZ    thrpt   20  7898101.929 ± 854734.733  ops/s
DecodingBenchmark.decodeFooMZ   thrpt   20   769205.462 ±  60756.223  ops/s
DecodingBenchmark.decodeFooPB   thrpt   20  1178288.024 ±  36754.823  ops/s
DecodingBenchmark.decodeFooZ    thrpt   20  1377253.293 ± 234093.569  ops/s
DecodingBenchmark.decodeFoosA   thrpt   20      796.463 ±    111.745  ops/s
DecodingBenchmark.decodeFoosC   thrpt   20     1607.345 ±     79.042  ops/s
DecodingBenchmark.decodeFoosMZ  thrpt   20    10599.926 ±   1889.855  ops/s
DecodingBenchmark.decodeFoosZ   thrpt   20    17261.595 ±    260.918  ops/s
DecodingBenchmark.decodeIntsA   thrpt   20     6230.473 ±    659.867  ops/s
DecodingBenchmark.decodeIntsC   thrpt   20    12751.050 ±    921.923  ops/s
DecodingBenchmark.decodeIntsMZ  thrpt   20    59862.336 ±   1785.270  ops/s
DecodingBenchmark.decodeIntsPB  thrpt   20   125009.494 ±   9799.178  ops/s
DecodingBenchmark.decodeIntsZ   thrpt   20    85695.355 ±  27599.620  ops/s

Fooが単発のcase classでFoosMap[String, Foo]intsVector[Int]です。 BarはZeroFormatter+フィールドをcats.Evalにしたものです(後述)。 AがArgonaut, Cがcirce、PBがScalaPB、MZがmsgapck4z、Zがscala-zero-formatterになります。

注意事項としては、元ネタのcirce-benchmarkはListだったのですが、ScalaPBのdecode用メソッド(mergeFromで調べよう)がVectorを使って生成してSeqをフィールドに持つようになっているので他のやつも合わせました。 あと、jmhの仕組み的にネストしたクラスが扱えないらしくScalaPBのベンチマークがとれていません。

ScalaPBは速いなぁ、というのがまず最初の感想ですね。 この中で唯一ジェネレータ系かつ生成されるコードがこれまた無駄がないので簡単なcase classであれば他を圧倒します。 sbtプラグインのおかげで10行程度(?)でプロジェクトの設定ができるし、gRPC対応してるしで、Scalaでの開発に限って言えばまず有力な候補に上がるの間違いなしです。

scala-zero-formatterはScalaPBと良い勝負をしているものの、バイナリサイズやIDLの利便性を考えるとどうなのでしょうねぇ。 後述のStage2あたりが差別化要因という話はありますが、さてはて。

msgpack4zはByteArrayOutputStreamを経由しているのがネックなのかな……あるいはベンチマークに使ったコードが微妙なのかも? これに関してはもう少し調査してみるつもりです。 でないとフェアじゃないので……。

JSON系が遅いのはしょうがない。 目的が異なる(?)ので、そもそもバイナリシリアライザと比較するものじゃない気がする。 ではなぜ比較したかといえば、circe-benchmarkをforkしたからなのでした。

ちなみにscodec-msgpackを比較対象にいれていないのは、そもそもスタックオーバーフローが解決できなかったという致命的な問題のせいです……なんとかしないと。

ScalaPBが早い理由

推測も混ざっているのでご注意ください。

  1. シリアライズの最初にバイナリサイズを計算
  2. サイズがわかっているので余計な操作が入らない
  3. scala-zero-foratterは「足りなくなったら増やす」戦略をとっているため随所でサイズチェックとArray[Byte]のコピーが発生する
  4. デフォルト値のときは何もしない
  5. 今回のベンチマークではそんなに関係ないけど一応
  6. ジェネレート時のインライン展開
  7. 生成されたコードのwriteTomergeFromに各フィールドのシリアライズ、デシリアライズ処理が手続き的に並んでいる
  8. 余計な呼び出しがないならそれだけ遅くならないということ
  9. このあたりでいくらか差が出る場合もある
  10. sun.misc.Unsafe?
    • 早いらしいという噂の彼
    • ScalaPBというよりは、JVM向けScalaPBランタイムが依存しているprotobuf-javaエンコードで使っている
    • とはいえ全体的な結果からするとあまり速さとは関係ない?(気のせい?)

scala-zero-formatterのStage2エミュレーション

ZeroFormatterの仕様には幾つかのStageがあります。 Stage2はObject、FixedSizeList、VariableSizeListのフィールドや要素のデシリアライズを遅延する仕様になっています。

で、これをScalaでどう実現するかですが……今回はとりあえずcats.Evalを使うことにしました。 ユーザが自前で定義する系なのでObjectや遅延リスト以外に適用して事故を起こす可能性もある……とはいえ、がんばらずにStage2の挙動を手に入れられるのでこれでいいかなーと。 equalsとかがまともに使えなくなる問題もありますが、比較したい時点で全部正格評価すべきだと思います。

バイナリのキャッシュ

C#のZeroFormatterにはバイナリをキャッシュして書き換えた部分以外はdeep copyする機構があって、これでシリアライズも省電力化することができます。 ZeroFormatterの仕様に存在しない仕組みとはいえ、こういう機能もできればscala-zero-formatterに欲しい……ということで、まずは単純にキャッシュするだけのAccessorというものを用意してみました。 case classの値が書き変わらないのであれば内部で溜め込んでいたバイナリをそのままシリアライズの結果として返します。 完全に横流し専用ですね。

そのうち部分更新の機能もサポートしたいのですが、ネストしたフィールドのcase classの変更をどうやって最上位のcase classにまで伝播させれば良いのかで詰まっていて当分先になりそうです……。 Lensを参考にしようと思っていましたがなかなかうまくいかないものですね。

まとめ

バイナリシリアライザのパフォーマンスチューニングは無限大に時間を消費するなーと。

とはいえ、もう少し何かできそうではあるので他言語版ともとも頑張ると思います。

ScalaとRust用のZeroFormatterライブラリを作り始めた

あけましておめでとうございます。 進捗どうですか? 私はダメです。

新年早々バイナリフォーマッターの話をします。

ZeroFormatterは、昨年の11月頃にneueccさんが公開したバイナリシリアライザーです。 詳しい話?は実装者の記事を読んでください。

ここで重要なのは上記記事の一文です。

と、いうわけで、言語がC#のみってのはさすがに普通に欠点なんですが、整備してみたんで多言語サポートよろしくお願いします、みたいな(?)。

おおなるほどな、と思いながら当時はスルーしていたのですが、年末に改めてREADMEを眺めていたら気力が湧きあがったので作ることに決めました。 まぁ、一番の決め手はCross PlatformでStageが明確に分かれていることですね。 なるほど、最初はeager-evaluationで良いなら徐々に育てれば良いかみたいな。

Scala

https://github.com/pocketberserker/scala-zero-formatter

現時点ではshapelessにべったりです。 あとなんとなくScala.js対応してます。 コードは汚いです(おい)。 ScalaのバージョンやJVMScala.jsかどうかで動かないものがあったりもするのですが、まぁ、そのうちどうにかします(願望形)。

以下簡単な説明

  • Object、Union、EnumをそれっぽくC#に似せて作った
    • ADTをフォーマットしたいならUnionを使うしかないという制限がありますが、まぁ、UnionKeyを見つける必要があるのでしょうがない
  • Formatterの自動導出
    • shapelessによるゴリ押し
    • そのため、生成に失敗した際のエラーメッセージが"could not find implicit parameter"という情報量のなさに…
  • 使い方はシンプル
    • ZeroFormatter.serializeZeroFormatter.deserializeを呼ぶだけ
    • このあたりはC#版がシンプルなAPIだったので引き継がれた感じ?

そして結構詰まったりもしました。

  • オブジェクトのデシリアライズ時にHListの各要素のFormatter型クラスが取得できない
    • シリアライズは対象の値があるのでその型からFormatterをimplicit pamrameterで取得できたが、デシリアライズに値はない
    • ないので、HList.fillWithnull.asInstanceOf[T]で無理やりHListを生成し、そこからimplicit parameterでFormatterを引っ張ることにした
  • Coproductの各型のFormatter取得に苦戦
    • HListと同じ問題だが、CoproductにfillWithはない(あったら困る)のでさらに苦戦
    • 結局以下の順序で無理やり取得した
      1. Coproduct.lengthを取得
      2. nat.RangeでHListを生成
      3. HList.foldRightで各要素の操作時にCoproduct.Atを使って型を導出
      4. 取得した型にたいしてGenericやHListを駆使してFormatterを取得する
  • Annotationやら継承ベースやらでScalaらしくない?
    • Indexというメタデータを持たせる方法がAnnotation以外に思いつかなかった…
  • case objectがCoproductでの生成対象にならない?のでEnumは仕方なくcase classのみに固定
  • UnionでKey用のFormatterインスタンスが取得できない
    • A <: Union[B]と書いていたらimplicit parameterが解決できなくなった
      • A <: Union[_]なら大丈夫
    • しょうがないので、Unionをtraitからabstract classにしつつ[T: Formatter]と定義し、key用のシリアライズ、デシリアライズメソッドを追加

StructやUnionを継承ベースにしたのは成功か失敗かいまいち判断ついてないですね…。 あと、自動生成にこだわらずにやれば年末年始休暇を全部溶かすことはなかったでしょう。

Scala版の今後の展望。

  • 型レベルIndex
    • Natを使って型の重複をコンパイル時に判別する
    • …ということをやろうとしたがops.nat.ToIntがどうしても解決できずに諦めている
  • スタック溢れを防ぐ
    • 特定の書き方をした時だけStackOverflowになる…
    • 同じ要素数、型定義でも別の書き方をすれば回避できているのが謎
    • TailRec版を実装している途中だがimplicit parameterが解決できずに困っている
    • 同じような話を発見したのでcachedImplicitを試したら動いた https://github.com/milessabin/shapeless/issues/345
    • ただし2.10.6ではコンパイルエラーになる…Unionもサポートできないのでそのうちdropする
  • Stage2サポート
    • 劣化Lensみたいなものを作る想定
  • Unsafe?
    • sun.misc.Unsafeを使えばもっと早くなることは明らか
    • とはいえJava 9との兼ね合いがあるからなぁ
    • Scala.jsやScala Nativeはたぶん別実装にわかれるんじゃないだろうか

Rust版

Rustはαくらいのときに少し触って、むーりぃーと投げ出して以降ほったらかしていたのですが、さすがになんかもう一つぐらい言語の手札を増やしたい感があったのでやるやる詐欺状態に…。

と、そんなときによく考えたらZeroFormatterというお題があるじゃないですか。 かつてmsgpackを言語入門のお題にしてきた身としては格好の題材ですね?

というわけで作り始めました。

https://github.com/pocketberserker/zero-formatter.rs

まだprimitive typeとstringくらいしかないですが…そのうち頑張る、ということで。

いまのところの感想としては、Scalaのshapelessとは違う意味でコンパイルが全然通らない…といったところでしょうか。 Rustのお作法がわからない状態で作っているのでそのうちRust強者に良い書き方を教わりたい…。

ZeroFormatterの所感

うまい感じにまとまっているなーというのが実装してみた現時点での感想ですね。 nullの扱いがNullableとその他の型で異なるのが困りどころではありますが、そこさえなんとかなればStage1の実装は容易です。 いやまぁ、最速を目指すなら(そしてStage2対応するなら)それだけではもちろんだめですが、初期実装が比較的簡単に作れることは重要なんじゃないかと思います。 ないなら作る、の敷居が低いのは良いことです。

というわけで所感列挙。

  • nullの規定が明確なこと
    • has valueか最初が-1かどうかで判断できるのでOptionを標準にもつ言語ならそっちにマッピングさせることができる
    • フォーマットによってはこのあたりが面倒な場合があるので…
  • Union
    • 昔欲しかった(切実)
  • バイナリサイズが富豪っぽく感じるけどトレードオフだし仕方なさそう
  • Indexは人によって好き嫌いあるかも?
    • とはいえそれはprotobufも同じだし、そういう時代の流れなのかも?

msgpackやprotobufなどとと張り合うならJSONとの相互変換もドキュメント化されているほうがいいんじゃないかとか、まぁいろいろありますが、そこはコミュニティができるかどうか次第なんじゃないでしょうか。

それ以外だとC#以外の実装がどこまで最速の座を奪えるかどうか、もあるのかな?

あと別言語での通信に使うとなればジェネレータが必要になりそうですが、あのSchemaなら誰かがえいやでプラグイン機構付きのものを作れってくれるでしょう…という楽観もあったりなかったり。

おまけ

F#拡張?知らない子ですね。

…いや、決して知らないわけでもissueを眺めるにとどめているわけでもないのですが…。 なんというかこう、ILとにらめっこすれば作れることはわかっているし技術的な問題はおそらくないはずだけど、そこに面白さはないんですよねぇ…判別共用体に対応させてパターンマッチできるようにするくらい? うーん、それだったら欲しい人がそのうち作るでしょう、みたいな。

…とか言ったら未来の自分にブーメランになりそうなのでここまでにします。

なお”気が向いたらErlangかElixirでも作ってみたいかも?”という気持ちはある。

おわりに

いろいろ書きましたが、ZeroFormatterはまだ全然実装が揃っていないみたいで、あなたがいますぐ実装すればその言語でのZeroFormatter第一人者になれるのでおすすめです。 最適化の勉強にもなりますからね。

scalaでmuscle assert的なライブラリの試作

Scaladogというテスティングフレームワークを以前から作ってみているわけですが、こいつでPersimmon.MuscleAssertみたいなことはできるのかなと思い作……ろうとして放置していたものを少し手直しして動くようにしました。

https://github.com/scala-kennel/hound

Diffの実装自体はただのshapelessのようだ。

初期はhttps://github.com/xdotai/diffがよさそうに見えたのでこのライブラリを使っていたのですが、所々使いづらかったりあまり更新頻度高くなさそうだったりScala.js対応されそうになかったりしたので「Diffくらい作れるでしょ」という軽い気持ちで自作に走りました。

なお実装が雑なのでcase classの型が異なる時に型名しか表示されないです。 あと、shapelessを使いサボった関係で、そもそもprimitive type + case classくらいしか表示できない問題があります……Scalaでリフレクションを頑張る体力がないです……。

Stringの個別diffはライブラリがぱっと探せなかったので非対応です。 よさげなライブラリがあれば教えて下さい(Javaのものでも構いません)。

assertion時にオブジェクトのdiffを取得するのは、たぶん有名どころのテスティングフレームワークでも実装できると思います。 が、まぁ需要はなさそうですし私は作る予定はないです。 ScalaTestにはpower assertあるので、基本はそっちで満足する人が多いんじゃないかなぁと。

対象をJSONに特化させるならdiffson等を使って実装する、という手もあります。

Scala.jsに対応したscodec-msgpackをリリースしました

この記事は予約投稿です。

id:xuwei さんが ささっと作業してくださったのでリリースしました。

https://github.com/pocketberserker/scodec-msgpack/tree/v0.4.1

v0.4.0はプロジェクトの設定ミスにより空のjarが降って来るだけなのでスルーしてください…。

なんでscala-jsに対応できたのか

  • shapeless は Scala.js をサポートしている
  • scodec-bits も Scala.js をサポートしている
  • 依存している二つのライブラリが Scala.js をサポートしたので、 scodec-core も v1.8.3 から Scala.js をサポート開始
  • scodec-msgpack は scodec-core のみに依存する、かつそんなに変なことはしていない
  • よってプロジェクト設定とテスト部分のみコストをかければ問題なく対応可能

といった形です。 shapeless が Scala.js をサポートしているのはなんというか、すごいですね。 マクロ的とか楽に対応できるものなのだろうか…?

TypeClassを使うようにした

ライブラリを作っておいてアレですが、私自身がscodec-msgpackを使っているというわけではないのでテキトーでいいかなと思ってメンテナンスをさぼり気味でした。

しかし、下記issueがきたので

Make primitive `Serialize`s implicit · Issue #8 · pocketberserker/scodec-msgpack · GitHub

重い腰をあげてimplicitな形式に変更しました。 変更ついでにshapeless.TypeClassも使うようにしたのでそれなりに自動生成できます。 インスタンス増やさないといけないのは…ぼちぼちやろうと思います。

その他

  • Extended -> Extensionにリネーム(破壊的変更なのでついでにやった的な)
  • FastOptJsでメモリをドカ食いするのでsbtoptsのmemをちゃんと設定しないとテストでstackoverflowします
  • Scala.jsのプロジェクト設定、特にクロスビルド設定をやったことがなかったのでちょっとしたところではまって難しい…
  • 次にメジャーバージョンにあげるあたりで1.0.0にすると思います

dog用のautodocライブラリを作った

Rubyにはautodocというライブラリがあります。

r7kamura/autodoc · GitHub

これのdog用のライブラリを作りました。

pocketberserker/dog-autodoc · GitHub

dog-autodoc用のsbtプラグイン実装中です…最低限部分できたので一旦リリースしました。

pocketberserker/sbt-dog-autodoc · GitHub

モチベーション

Scalaにはplay-autodocというものが存在する。

krrrr38/play-autodoc · GitHub

でもこれ、Play Framework用なんですよね。ということで

  • Playに依存しないものにしたい
  • どうせだからクライアントは切り替えたい -> httpzを使おう

というのがモチベーションです。

苦労した点

  1. sbtから出力ディレクトリを設定しつつ
  2. テストを一回だけ実行し
  3. 実行後に出力する

この流れをどうやってみたすかかなり悩んだ。 というか、当初の想定より随分と変わっている(テストの実装ミスしてて当初想定していた残骸がゴミコミットとして残ってしまった…orz)。

最終的には、

  • Autodoc用のTestFrameworkを登録しておく
  • AutodocMarker型の値が結果となっているケースのみ特別な処理をしてからイベントを登録する
    • Autodocは実行したテスト結果とテストケース名から文字列をジェネレートできる
    • Event#fullyQualifiedNameにクラス名を入れるので、あとは出力結果(文字列)をどうにかしてEventに持たせれば保存に必要な情報が揃う
    • ところで、出力が可能なのはテストに成功したときのみ
    • テスト成功時には通常Event#throwable.isDefinedfalseを返すはずである。なぜなら、テスト成功なのに例外が投げられている状態は矛盾しているからだ。
    • 実際JUnit Reporterはこの組み合わせを無視している https://github.com/sbt/sbt/blob/v0.13.9/testing/src/main/scala/sbt/JUnitXmlTestsListener.scala#L84
    • つまり、この使われないであろうEvent#throwableに出力を押し込んでおくことで、ファイル書き出しをsbt内だけで完結させることができる
  • https://github.com/sbt/sbt/blob/v0.13.9/testing/src/main/scala/sbt/TestReportListener.scala#L9 を使ってイベントを取得してテスト終了時に保存

という形に。ひどい実装だ…。

こういう形になった最大の要因は型パラメータが邪魔をしてリフレクションでの取得 or 型の変換が難しかったからですね。以下そのときのxuwei-kさんのアドバイス。

こういうときはType erasureが辛く感じます…。 結局、autodocのためにdog自体をいじるのはなんとなくいやだったので採用しませんでした。

まとめ

dogの作りが従来のユニットテスティングフレームワークと異なるからこういうときつらい…でもがんばる。

今後は安定かとかconsole用のWriterを復活させるかどうかあたりですかね。 あ、あとrename…ひどい命名ばかりしているので良い名前母種中です。

dogというScala用テストフレームワーク(?)を作ってみた

https://github.com/pocketberserker/dog

このフレームワークPersimmon の移植というか Persimmon に存在していた型をよりMonadとかそういう方面に倒してみようというものです。

テストという存在の型について考えてみる - pocketberserkerの爆走

上記に途中経過は書きましたがそれから数日と経たずにいくつか変更が入ってます。

特徴

  • type TestCase[A] = Kleisli[TestResult, Endo[Param], A]
  • type AssertionResult[A] = NotPassedCause \/ A
  • scalapropsフレンドリー
  • イミュータブル、ミュータブルを意識しましょうてきな

モジュールとかその他リポジトリについて

  • dog-core
    • 基本となる型と関数をまとめたもの
    • TestCase単体でタイムアウト設定して実行できるように scalaz.concurrent に依存している
    • 上記の通り TestCase は Kleisli なので合成できる
    • AssertionResult を合成することでいわゆる Soft Assertion を実現している
  • dog
    • sbtのtest-interfaceに依存
    • scalapropsの実装をfork(実際にはコピーしたけど)して改修
    • scalapropsとは異なり非同期実行しても問題ないつくりになっているがさてはてどうしたものやら
  • gen
    • scalaprops-genに依存
    • scalaprops.Gen を使って入力値だけいい感じに生成してもらう
    • 入力と期待値の集合は作れるけど性質までかけそうにない場合とかに…ぶっちゃけパラメタライズテストのかわり
    • 固定値で回すくらいなら数撃ってあてようの方針
  • props
    • scalaprops-coreに依存
    • PropertyやPropertiesとTestCaseの変換を目的としている
  • sbt-dog 9割方sbt-scalapropsのコピー。scalaprops様様である(MITライセンスで助かった…)
    • sbt-scalapropsに比べてぴーきーな挙動になりやすい
  • dog-examplesサンプル集。雰囲気がつかめるかも?

ここまでで実装期間6日です。 正直ScalazとScalapropsがなければここまで楽に実装はできなかったと思うので xuwei-kさん ++

作った理由を改めて

  • テストの合成とか型とかについてちゃんと検証したかった(某交流会の資料作成絡み)
  • PersimmonのVS拡張や某ライブラリの移植に難航しているので息抜きしたかった
  • 深夜のテンション

名前はリポジトリ作るときに"ドッグフーディングしたい"というのと"Co猫"という謎ワードがふってきて悩んだ結果であり、特に猫に張り合っているわけではないです。

今後の計画

  • Property#forAll などをラップして性質を満たす時にGen#sampleをテスト結果として束縛できるようにしたい
  • Scala.js対応
    • とはいえこれはリフレクションを使って実装している現状うまくいかない
    • なので、scalaprops共々forkして別リポジトリで実装するつもり
  • やりたいことあればてきとーに

こういうものがほしい、とかあればissueに登録していtだけるとうれしいです。

ScalaMatsuriにこれで応募してみるという手もあるが、さてはて。