こんにちは、11日目の投稿になります。
Advent of Codeばっかり解いててもつまらないので、今日は私の作ったp6-Chart-Gnuplotをどういう感じで作っていったのか紹介しようと思います。
1. モチベーション
去年のアドベントカレンダーでも紹介記事があったように、Perl6のグラフ描画ライブラリにはSVG::Plotがありました。
しかし、matplotlibやgnuplotなどの高機能な描画ライブラリと比較すると、どうしても力不足な印象が否めなせんでした。
そこで、Perl6初の本格的な描画ライブラリとして、gnuplotのバインダを作ることにしました。
2. コードリーディング
描画ライブラリというものを全く作ったことがありませんでした。
いろいろ着手していく前に、ほかの競合ライブラリのコードリーディングをやって知見を貯めていくところから始めました。
具体的には、まず開発規模が近そうなのでperl, goのgnuplotのライブラリを読みました。
そして、これだけ使われてればさすがにいい感じのテストが書いてあるだろうと思ってmatplotlibを読みました。
Chart-Gnuplot
大先輩であるPerl 5のライブラリです。
プロダクトが出来上がるとPerlと確実に比較されるので読むことにしました。
- このライブラリは下記のような特徴があるようでした:
- 細かな描画オプションはChart::Gnuplot::DataSetに格納する
- thawに描画オプションを読み取る操作をやらせる
- 実行のためのスクリプトはファイルにいったん保存する: https://metacpan.org/source/KWMAK/Chart-Gnuplot-0.23/lib/Chart/Gnuplot.pm#L236
- executeでシステムコールによってgnuplotコマンドを実行する
- あらかじめファイル保存されたスクリプトとテスト時に生成保存されたスクリプトのdiffを取ることでテストしている
go-gnuplot
規模が小さいので手が付けやすそうでした
- このライブラリは下記のような特徴があるようでした:
- 二回目以降のplot呼び出しはreplotで処理する
- プロットする点のデータはファイル書き出しする: https://github.com/sbinet/go-gnuplot/blob/master/gnuplot.go#L195-L208
- Cmdを通じてgnuplotのsubprocessに命令を送る: https://github.com/sbinet/go-gnuplot/blob/master/gnuplot.go#L85
+ PlotXYなどの関数も最終的にはCmdを呼び出すことで命令を送る: https://github.com/sbinet/go-gnuplot/blob/master/gnuplot.go#L194
+ PloxXYなどの専用のインタフェースの用意されていない場合は、クライアントはCmdに命令文をベタに指定することで処理を行う - テストがない :)
glot
goのライブラリで特に人気があるようなので読むことにしました。
- このライブラリは下記のような特徴があるようでした:
- ところどころAPIは違うかもしれないが、おそらくgo-gnuplotがベースになっていてアプローチがかなり近い
- エラー処理だけはテストされている
matplotlib
単体テストを一体どのようにおこなっているのか。この一点にだけ関心があったので読みました。
結論から言うと、maptlotlibで行われている単体テストは仕様化テストです。コードをそこまでしっかりと読まなくても下記ページに説明がありました:
https://matplotlib.org/devel/testing.html
具体的には下記の手順になっているようです:
- 今後テストに使用したい描画オプションを指定してmatplotlibによりsvg画像を生成する(※pngはファイルが大きいので結局svgしか使わない)
- ベースとなるイメージを保存するためのフォルダにそのsvg画像をコピーする
- 1)で指定した描画オプションと同様のオプションを指定して画像を生成し、2)のフォルダ内の対応する画像とのdiffを取る
3. フレームワーク
コードリーディングした結果を参考にして、どういう枠組みで解くべきか決めました:
- 基本的にgo-gnuplotのアプローチを採用する
- i.e. 専用のインタフェースがある場合はそれで、そうじゃない場合はベタに命令文を指定してもらう
- ファイル書き込みはダサいのでやらない
- テストはChart-Gnuplot(Perl)の方式でおこなう。ただしファイル書き込みは行わないので、変数に命令文を保存して、想定した命令文と一定しているかどうかをチェックするようにする
4. 実装
フレームワークがだいたい定まったのでいよいよ実装です。
はまったポイントをいくつか紹介しようと思います
Procでgnuplotを呼び出すとハングする
- Proc::Asyncに切り替えて乗り切りました
Proc::Asyncを使うと副作用があってテストが書けない
- 引数に関数をとって書き込み先を指定できるようにしました。
- テストのときはgnuplotのプロセスではなく、リストの変数が命令文の書き込み先になります。
https://github.com/titsuki/p6-Chart-Gnuplot/blob/0.0.2/t/05-label.t#L25
プロット対象のデータをファイル書き込みしたくない
- gnuplotの公式ドキュメントをちゃんと読むとメモリに保存する方法が書いてありました。
- こんな感じで書きました: https://github.com/titsuki/p6-Chart-Gnuplot/blob/0.0.2/lib/Chart/Gnuplot.pm6#L88-L93
nohidden3d などのオプションがPerl6の否定の書き方と一貫性がない
- FalseOnlyというFalseしか格納できないsubsetを作りました
-
FalseOnly :$hidden3d
というようにすることでnohidden3d
をPerl6っぽい感じで指定することができるようにしました
replotが効かない・・・?
- まさにこの問題に当たりました: https://stackoverflow.com/questions/11044851/gnuplot-using-replot-with-png-terminal
- こんな感じで記述して回避しました: https://github.com/titsuki/p6-Chart-Gnuplot/blob/0.0.2/lib/Chart/Gnuplot.pm6#L125-L126
ほかにもいろいろありました。
特に、自分のgnuplotへの理解の浅さがコードに露呈するのが怖かったので、何のスキルが伸びるでもなく、毎日のようにgnuplotの公式ドキュメントを読む日々はつらかったです。
5. テスト
- 生成された命令文が想定したものと一致するかどうかをテストしました。
- 画像自体のテストはしていません。(それはgnuplotがやるべきだと思います)
6. リリース後
某NASAの人がスター付けてくれました。
一方APIがダメダメだというコメントがirclogに残されていたので微妙な気分です。
ぜひPRお待ちしております
以上11日目の投稿でした。