ZeroFormatter.FSharpExtensionsのF# 4.1対応

FSharp.Core 4.1.0がNuGetにpublishされていたので、ZeroFormatter.FSharpExtensionsでF# 4.1の型を扱えるようにしました。 ついでにプロジェクト構成を.NET Core SDK RC4のものに全面修正しています。

NuGet Gallery | ZeroFormatter.FSharpExtensions 0.3.0

いやまぁ、正確には.NET Coreでのビルドに失敗していて(ビルドは通るけど空のdllができていた…)対応せざるをえなかっただけなんですけどね(ひどい)。

対応した型

  • struct tuple
    • 内部的にはSystem.ValueTuple
    • Nullableは非対応
  • struct record
  • struct union
    • Resultはここに属する

わかりやすいですね。

Nullableなstruct tuple用のFormatterを用意しなかったのはF#だからOptionでいいんじゃない、みたいな理由です。 C# 7のことを考えれば本家がValueTupleをサポートすべき事案なのですが、忙しそうなのでいつ次のバージョンがリリースされるかわからない…。 pull reqしようにも.NET Core SDK preview2が手元にないのでビルドもままならないので「project.jsonめ…」という気持ち。

余談

.NET Core対応させようとしたときに下記の問題にぶつかりました。

https://github.com/Microsoft/visualfsharp/issues/2455

FSharp.Core 4.1.0のnetstandard1.6ではFSharp.Reflection.FSharpTypeFSharp.Reflection.FSharpValueのいくつかのメソッドが存在せず、型拡張で定義されているメソッドにはC#からアクセスできないのでレコードや判別共用体を判定する手段がなくあわや詰みかけたという…。 C#からF#用のメソッドを呼び出す人はそうそういないので気付かないのも仕方ない。 幸いドンさんから回避策を教えていただけたのと(ただしリフレクションを使う…)、F#自体にも変更がはいったのでバージョンがあがれば心配しなくてすみます。

それにしてもこのライブラリ、開発中にいろんなものを踏み抜くなぁ。

F# のJSON事情

現時点でF#のJSON事情をぱっと思い出せなかったので、知っている範囲でメモしておくことにします。

あらかじめ断っておくと、私見にまみれているかつ抜けているライブラリがあるかもしれません。

DataContractJsonSerializer

標準にあるやつ。 当然ながらF#のいくつかの型には対応していない(意図しない出力になったりエラーになったり?)。

一つのプロジェクトに閉じていて他のライブラリを使うまでもないくらい小さいJSONを扱いたい時に使える? FSDNはまさにそんなJSONだったのでこいつを採用しています。

*** 追記

拡張を用意してあげればもちろんF#の型も対応できます(ただ拡張ライブラリ入れるなら別のライブラリを選ぶ…)。

余談

Suave本体に含まれているSuave.Jsonモジュールは内部でDataContractJsonSerializerを使っています。 例外はmapJsonWith関数で、こいつはdeserializeとserialize用の関数を渡す形になっています。 つまり、Suaveで他のJSONライブラリを使いたいならmapJsonWithを用いて各ライブラリの関数を作るか、直接そのライブラリを使うかになります。 まぁ、mapJsonWithを使わない場合もライブラリの入れ替えが楽になるようにオレオレJsonモジュールを作るのが良いと思います。

Json.NET

http://www.newtonsoft.com/json

.NETでJSONといえばこれでしょ、みたいなやつです。 いろんなライブラリ、フレームワークがこいつを使っているので必然利用頻度が高まります。

素の状態でF#の型をシリアライズすると望んだものでないデータが出来上がるので注意が必要です。

個人的なあれをいうと、こいつを使う場合はclassや.NET標準のデータ型のみで型を定義しますね。 F#用の拡張を用意するのは面倒だし、かといって他のライブラリがメンテナンスされているかというとうーん…となりやすいので。 レコードやlist、その他もろもろを使わない覚悟が必要になりますが、まぁAPI用の型と内部で使う型って別物になりがちなので入口出口くらいしかきにならないというか。

FSharpLu.Json

https://www.nuget.org/packages/Microsoft.FSharpLu.Json/

珍しくMicrosoft organizationに存在するF#プロジェクトでJson.NETに依存しています。 存在感は薄い。

Newtonsoft.Json.FSharp

https://github.com/haf/Newtonsoft.Json.FSharp

SuaveコミッターのHenrik Feldtさん作……ですがdescriptionに以下の注意書きがあります。

I recommend using Chiron instead of Newtonsoft

どうやら後述するChiron推しのようです。 使う機会はないでしょう。

Chiron

https://github.com/xyncro/chiron

HaskellのAeson的なライブラリです。 Lensがおまけでついてきます。

そっち系に慣れている人は(アプリカティブスタイルも使えるので)使いやすいかもしれませんが、慣れない人は敬遠しそうな感じ。

FSharp.Data

https://github.com/fsharp/FSharp.Data

TypeProviderの例としてよく知られているやつですね。 型を自動生成してくれるのはこいつくらいじゃないでしょうか(JSONは往々にして自動生成が役立つことってあまりない気もしますが…)。

解析向けに使うには強いけどAPIに使うには向かない印象を私は持っています(あくまで個人の感想です)。

FsPickler.Json(番外)

https://www.nuget.org/packages/FsPickler.Json/

機能がモリモリなバイナリシリアライザのFsPicklerのJSONプラグイン的なやつ。 JSONライブラリというわけではないので番外扱い。

ドキュメントに以下の記載があります。

FsPickler is NOT:

  • a general-purpose XML/JSON/BSON framework.
  • a library designed for cross-platform communication.
  • a library designed with version tolerance in mind. Avoid using for long-term persistence.

まぁ、そりゃそうですよね。

パフォーマンス

知らぬ。

いや、重要なのは理解しているのですがよっぽど遅くない限りは速度気にならない部分でしか使わない…真面目に速度が必要ならバイナリシリアライザを使うからなぁ。

真面目に使うつもりの人は計測しましょう。

まとめてきな何か

正直、SuaveでJSON APIを組み立てる時にいつもライブラリ選定に悩まされます。 どれも一長一短あるし、他の言語を触っている身としては「auto deriveしつついい感じに使わせてくれ〜」みたいな気分になるわけですよ。

一応個人的な使い分けを書いておくと

  • Chiron
    • 個人的な慣れの問題で使う
  • FSharp.Data
    • Readerしか必要ない時
  • Json.NET
    • 他のライブラリがこれに依存している時は問答無用で採用
    • 同じプロジェクトで複数のJSONライブラリを使いたくない
  • DataContractJsonSerializer
    • 上記を使うことすら億劫なとき

こんな感じですかねぇ。

結局F#でもJSONライブラリは氾濫しているのでした…つらい。

F# の型をZeroFormatterで扱うための拡張ライブラリを作った

本日もZeroFormatterネタです。

年始の記事で

欲しい人がそのうち作るでしょう

などと書いていたら見事にブーメランでした。 欲しくなったというよりはF#erとしての意地(謎)という感じですが。

本家ライブラリだけでどこまで書けるのか?

primitiveな型やコレクションは普通に動きます。 .NETですからね、そりゃそうだ。

問題はF#固有の存在やObjectです。 C#とは書き味が異なるので検証してみましょう。

試したバージョンはZeroFormatter 1.6.2です。

Measure

[<Measure>]
type kg

let bytes = ZeroFormatterSerializer.Serialize(input)
ZeroFormatterSerializer.Deserialize<int<kg>>(bytes)

これはtype eraseされるので普通に動きます。

Object

[<ZeroFormattable>]
type MyObject() =

  abstract member MyProperty1: int with get, set
  [<Index(0)>]
  default val MyProperty1 = 0 with get, set

  abstract member MyProperty2: int64 with get, set
  [<Index(1)>]
  default val MyProperty2 = 0L with get, set

  abstract member MyProperty3: float32 with get, set
  [<Index(2)>]
  default val MyProperty3 = 0.f with get, set
  • virtualを要求するためabstract memberでプロパティを定義する
  • デフォルト実装のほうにIndexAttributeをつける必要がある
    • abstractなほうにつけると実行時エラー
  • ここではvalを使ったけど別に他の書き方でも問題はない

proectedがないからどうすんべ、みたいな問題は残りますが気にしてはいけません。

Structや.NET的なもの

.NETに存在する型やStructはだいたいおんなじ感じです。

tupleがStruct?で実装されていることに面食らうかもしれません。 F#だとtupleがnullになることなんてまず考えませんからねぇ…。

FSharp由来の型

実行時エラーになります。

  • Set, MapはICollection判定されるが、これらはnew ()制約を持たないため実行時に制約違反エラー
  • F#のlistはマッチする型が見つからない?
  • レコードはStructではない && 0引数のコンストラクタももたないのでObjectの条件を満たさず実行時エラー
  • 判別共用体は当然マッチする型がない
  • unitも(ry

まあ、BuiltInのものだけでは限界があるわけです。

FSharp用の拡張

https://github.com/pocketberserker/ZeroFormatter.FSharpExtensions

というわけで、上記の様々な問題を解決するための拡張ライブラリを作りました。 ZeroFormatter.FSharp.ZeroFormatterSerializerを使うとF#な型もシリアライズ、デシリアライズできるようになります。

基本姿勢としてF#の型はStage1のWireFormatの域をはみ出さないように作ってあるので、F#の判別共用体をシリアライズしたバイナリを他言語でUnionとしてデシリアライズする、なんてことも可能です。

判別共用体とレコードはeager-evaluationです。 virtualにできないのでObject的なデシリアライズができないんですよねー。 あとこの2つはStructなフォーマットに落とし込んでいる関係でVersioningをサポートしていません。 Versioningを使いたいならObject+アクティブパターンでどうにかするのが吉です。 マイクロサービス的にV1とV2のサーバをぽんぽこ立ち上げて投げ分ける、という手段をとれる人はVersioningにこだわらなくても良い、のかなぁ…ヨクワカリマセン。

注意点としてはoptionを使わない限りnull safeにならないことですね。 バイナリ仕様上、nullが返ってくるのは避けられないことなのです。 うっかりするとレコードや判別共用体にnullが突っ込まれるので、nullなものが飛んできそうならoptionで包んでください。

あと、ZeroFormatter 1.6.2の時点では判別共用体と同様のUnionを定義してもそのままだとシリアライズ、デシリアライズできません。 原因は把握していて、structの場合引数なしのコンストラクタがなくnullになるので落ちます。 自力でその型専用のFormatterを定義してregisterすれば回避できるかと思いきや、組み込みのFormatterが優先されてしまうため先ほどと同じエラーに遭遇します(プルリクはだしたhttps://github.com/neuecc/ZeroFormatter/pull/57)。 最終的にResolverでひと工夫すれば実行できるようになりますが、余計なFormatterを1つ経由しないといけないのでつらい…改善されることを祈りましょう。

内部実装

実装はフルC#です、はい。 ミュータブルばりばりな予感がしたのでF#で実装すること自体が頭になかったですね。

判別共用体の実装はIL以外の部分が地味に面倒でした。 ちなみにデシリアライズだけボクシングが挟まるので遅い、かも。 私の力ではFSharpValue.MakeUnionを使って生成する方法しか無理だったのです…。

なおILGeneratorを使ったコードを初めて書きましたが、まぁ、手本とILSpyがあればあとはpushしてpopするだけなので(おい)、小さめのコードならそんなに苦労はないですね。 意味不明な実行時例外が飛んでくるなんてよくあることなので気にならなかった(?)です。 テストが通らなかった時は一度落ち着いてから脳内stackを駆使するに限る。

Analyzerは?

Visual Studioで消耗したくない…はともかくとして、今のところ労力に見合わないので実装予定はありません。 IDEでIndex重複をエラーにするとか自動でZeroFormattableな型に変更するとかすべきなのでしょうけどね…そういうのは実際に利用する機会がでてきたときにでも作ろうかと。

Unity対応とかコードジェネレータは?

zfcに手を入れるのは大変そうだし(仮にやっても破壊的変更しすぎてマージしてもらえる気がしない)、かといって1から作るのもなぁ…。 ZeroFormatterなバイナリASTを用意してpluginと通信する、とか漠然と考えてはみたものの、無限に時間をもっていかれそうな気がしたのでいったん考えることをやめました。

とはいえうーん、他言語との通信を考えるとやっぱりジェネレータが必要な気がする…。 msgpackのデメリットの一つは(実質)標準IDLがないことに起因してジェネレータが存在しないことだと思っています。 ZeroFormatterは(C#という)スキーマがあるのでそのあたり事情がことなるけれど、現状は言語ごとに型を手書きするしかないので2度手間だし間違えそう、とか考えるとあまり状況は変わらないのかなと。

とりあえずたたき台を作ってみるべきなのかなぁ…悩まし。

おわりに

ZeroFormatter系ライブラリはこれで3つ目。 なんというか、バイナリシリアライザはほんと色んな意味で訓練になります、はい。

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第一人者になれるのでおすすめです。 最適化の勉強にもなりますからね。

Persimmonの.NET Core対応

これはF# Advent Calendar 18日目の記事です。 そして.NET Core Advent Calendar 18日目の記事でもあります。 盛大に遅刻しました。

qiita.com

qiita.com

今回はPersimmonという、私が開発にかかわっているF#向けテスティングフレームワークの話をします。

スティングフレームワークを.NET Coreに対応させるためにやるべきことはいくつかあります。

  1. 本体を.NET Coreに対応させる
  2. test communication protocolに従ってランナーを実装する
  3. 周辺ライブラリを.NET Coreに対応させる

Persimmonはこのうち最初の二つが完了しているので、他に依存のないプロジェクトであれば.NET Core版のPersimmonを試すことができます。

dotnet-test-persimmon

.NET Core CLI用にdotnet-test-persimmonを実装しました。 内部実装はF#非依存です(ランナー自体のテストはF#で書いていますが)。

CLIでの実行は特に問題ないのですが、Visual Studioのテストエクスプローラー連携がまだ若干怪しい挙動なのでお試し程度にどうぞ。

周辺ライブラリ

ほぼ未対応です。 というのも、まだF#自体の.NET Coreサポートの先行きが不透明なので労力を割くべきか決めあぐねているからです。

遅刻したわりに短い記事になってしまった…。

コンピュテーション式の展開結果を可視化するツールComVuを作った

この記事はの13日目です。

qiita.com

今回は過去に作ったComVuというツールの話をします。

https://github.com/pocketberserker/ComVu

nugetでダウンロードできます。

これはなに

ComVuはコンピュテーション式を機械的に展開し、結果を表示するライブラリとツールの総称です。

名前の由来ですが、F#談話室終了後の食事の席で「こういうツールほしい」という話になった際、「こんぴゅてーしょんえくすぷれっしょんびじゅあらいざー…昆布だ!」という流れで決まりました。

ComVu.Coreが解析部分、ComVuがWPFによるツールとなっています。

外部ライブラリの解析

FSharp.Compiler.Serviceを利用してコンパイルや解析を行っているので、#I#rを使えば外部ライブラリのコンピュテーション式も解析可能です。

sequence expressionsの疑似展開

sequence expressionsの実装はコンピュテーション式ではありませんが、にたような形式で作れるよという意味合いを込めて展開結果を表示できるようにしています。

問題点

  • 一部のオペレータや関数名の表示が微妙
    • 記号や関数名ではなくCompiledNameで表示されてもわかるでしょ、という気持ちがあったためさぼりました
  • インデント深すぎ
    • YC.PrettyPrinterを使ったらこんな感じに…外部からインデント幅を指定することが簡単にはできそうにないので、独自実装するしかないのかなぁ
  • Windows以外では使えない
    • ComVu.Coreを使えばあとはUIの問題だけなので、どなたか作ってください

余談

作ったは良いものの、私自身はツール無くてもどうにかなっているので拡張する気力があまり起きないんですよね…。 とはいえよさそうな提案があれば実装するつもりはあるので、issueお待ちしています。