`dotnet new`用のテンプレートエンジン
不定期.NET Core SDK周辺調査メモ。
GitHub - dotnet/templating: This repo contains the Template Engine which is used by dotnet new
かつてはdotnet new3
と呼ばれていたらしい。
RC4時点でこいつがdotnet new
で使われるのでどうでも良い知識ではある。
https://github.com/dotnet/templating/tree/c3634ad9fe235fa23ed1a76149bf24f3fb9c41c0/template_feed
このあたりで標準テンプレートが管理されている。
現時点では隠しコマンドだったり、未実装なコマンドもちらほら。
キャッシュ情報は~\.templateengine\dotnetcli
あたりのようだ。
ja-JP.templatecache.json
やtemplatecache.json
を見ればどんなテンプレート情報を編集できる(が、手作業の修正はおすすめしない)。
NuGetにテンプレートをpublishしておけばいい感じに選べるようになる未来がくるのだろうか? Visual Studio用のテンプレートを作らなくてもVisual Studioで使えるとかであれば結構便利そうだ(実際どうなのかは知らない)。
F# の型拡張を使って定義したメソッドを C# から呼び出す
Donさんに教えてもらいました。
リフレクションを使って呼び出すだけなのでC#限定というわけではないです。
using System.Reflection; using Microsoft.FSharp.Reflection; ~ 略 ~ typeof(FSharpReflectionExtensions).GetTypeInfo() .GetMethod("FSharpType.IsRecord.Static") .Invoke(null, new object[] { typeof<string>, null }); // 第2引数のoptionを指定するつもりがないなら、.NET的にはnullでもOK
型名.メソッド名.Static
という形式になるらしい。
ILには存在するものの、C#がドット付きメソッドを呼び出せないのでこういう方法をとる必要があるようです。
使う機会はないでしょうがまぁ勉強になった、ということで。
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.FSharpType
やFSharp.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:
まぁ、そりゃそうですよね。
パフォーマンス
知らぬ。
いや、重要なのは理解しているのですがよっぽど遅くない限りは速度気にならない部分でしか使わない…真面目に速度が必要ならバイナリシリアライザを使うからなぁ。
真面目に使うつもりの人は計測しましょう。
まとめてきな何か
正直、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系)。
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でFoos
がMap[String, Foo]
、ints
はVector[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が早い理由
推測も混ざっているのでご注意ください。
- シリアライズの最初にバイナリサイズを計算
- サイズがわかっているので余計な操作が入らない
- scala-zero-foratterは「足りなくなったら増やす」戦略をとっているため随所でサイズチェックと
Array[Byte]
のコピーが発生する - デフォルト値のときは何もしない
- 今回のベンチマークではそんなに関係ないけど一応
- ジェネレート時のインライン展開
- 生成されたコードの
writeTo
やmergeFrom
に各フィールドのシリアライズ、デシリアライズ処理が手続き的に並んでいる - 余計な呼び出しがないならそれだけ遅くならないということ
- このあたりでいくらか差が出る場合もある
- sun.misc.Unsafe?
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のバージョンやJVMかScala.jsかどうかで動かないものがあったりもするのですが、まぁ、そのうちどうにかします(願望形)。
以下簡単な説明
- Object、Union、EnumをそれっぽくC#に似せて作った
- ADTをフォーマットしたいならUnionを使うしかないという制限がありますが、まぁ、UnionKeyを見つける必要があるのでしょうがない
- Formatterの自動導出
- shapelessによるゴリ押し
- そのため、生成に失敗した際のエラーメッセージが"could not find implicit parameter"という情報量のなさに…
- 使い方はシンプル
そして結構詰まったりもしました。
- オブジェクトのデシリアライズ時にHListの各要素のFormatter型クラスが取得できない
- Coproductの各型のFormatter取得に苦戦
- HListと同じ問題だが、CoproductにfillWithはない(あったら困る)のでさらに苦戦
- 結局以下の順序で無理やり取得した
- Coproduct.lengthを取得
- nat.RangeでHListを生成
- HList.foldRightで各要素の操作時にCoproduct.Atを使って型を導出
- 取得した型にたいしてGenericやHListを駆使してFormatterを取得する
- Annotationやら継承ベースやらでScalaらしくない?
- Indexというメタデータを持たせる方法がAnnotation以外に思いつかなかった…
- case objectがCoproductでの生成対象にならない?のでEnumは仕方なくcase classのみに固定
- UnionでKey用のFormatterインスタンスが取得できない
StructやUnionを継承ベースにしたのは成功か失敗かいまいち判断ついてないですね…。 あと、自動生成にこだわらずにやれば年末年始休暇を全部溶かすことはなかったでしょう。
Scala版の今後の展望。
- 型レベルIndex
スタック溢れを防ぐ特定の書き方をした時だけStackOverflowになる…同じ要素数、型定義でも別の書き方をすれば回避できているのが謎TailRec版を実装している途中だがimplicit parameterが解決できずに困っている- 同じような話を発見したのでcachedImplicitを試したら動いた https://github.com/milessabin/shapeless/issues/345
- ただし2.10.6ではコンパイルエラーになる…Unionもサポートできないのでそのうちdropする
- Stage2サポート
- 劣化Lensみたいなものを作る想定
- Unsafe?
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第一人者になれるのでおすすめです。 最適化の勉強にもなりますからね。