覇権を取るパッケージ作成

bicycle1885氏(以下、bicycle1885):飛び入りでの発表なんですが、主催者の佐藤建太といいます。タイトルは「覇権を取るパッケージ作成」ということで、私がやっているパッケージの作り方の戦略について、みなさんに共有しようかなと思います。

まず自己紹介です。

いま大学院生で、博士課程にいます。専門はバイオインフォマティクスといって、バイオロジーの、生物学の研究から出てきたデータを、ソフトウェアで解析するような分野です。東大に所属していて、理研にもちょくちょく行ってるので、そのどっちかにいるということですね。

すごいJuliaマニアでして、(バージョンが)0.1か0.2ぐらいのところからずっと使ってて、もう5年以上になるというマウンティングをしておきます(笑)。たぶんJulia歴は大多数の人より長いと思います。

JuliaConにもいままで3回出ていて、参加しなかった、というか参加できなかった回は最初と今年だけですね。ハイパーパッケージクリエイターを自称していて、Juliaでパッケージをたくさん作ってます。

今日はドタ参で、お昼に1枠空くことがわかったので、急遽話を聞きながらスライドを作ってました。

さて、本日の内容です。

まず、どういうパッケージを作ってるかを紹介して、それをどうやって作ってるのかについて、簡単にお話ししようと思います。そして、どうすればJulia界で覇権を取るというか、みなさんに使ってもらえるようなパッケージを作れるのかという話を、簡単にしていきます。

開発しているJuliaパッケージ

(スライドを指して)これが、開発しているパッケージです。

私が始めたものでないのも1個あるんですけど。

一番上がBio.jl、バイオジュリアといって、バイオインフォマティクスのためのJuliaパッケージを作るものです。210スターで、私が関わってるものでは一番多い。

まあ、まだ新しい言語ですので、あんまりパッケージにスターがつかないんですけど、こんな感じのものを育てています。いまは、バイオジュリアは1個にするとすごく大きいので、細かく分割してパッケージにしてます。

その下はDocOpt.jlというもので、コマンドライン引数のパーサです。48スターぐらいですね。一番最初に私が公開したパッケージです。ただ、(Julia)1.0に対応できてないです。ごめんなさい。

Automa.jlは、正規表現からJuliaコードへコンパイルするコンパイラみたいなものです。これも40スターぐらいついてて、Stefan Karpinski、Juliaを作ったビッグ3の1人からもスターをもらってるという、友達に自慢できるパッケージです。

あと、EzXML.jlという、XMLフォーマットの処理ライブラリも作ってます。これも32スターぐらいあって、けっこう多いですね。マジ便利です。

TranscodingStreams.jlはデータの変換ライブラリです。これは去年作ったやつで、19スターぐらい集めています。gzip、zstd、xz、lz4、bzip2、base64などを、すべて同じインターフェースで使えます。

他にもいろいろなパッケージを、細かいものもあわせるとたくさん作っていますが、主に上げたものをスター順に並べると、こんな感じになります。

なぜパッケージを作るのか? 登山家じゃないですけど、そこにパッケージがないからです。パッケージがあるから作るんじゃなくて、ないから作ります。

基本的に、自分がほしいものをパッケージとして作っています。ほしいものがないから作ってるわけですね。既存のパッケージがあるにはあるけど、ちょっと自分の用途には向いてないときに、自作するということです。もちろん、自分がほしいパッケージですので、自分で使うことを前提として書いています。

EzXML.jl

EzXML.jlという、XMLを扱うパッケージについて紹介しようと思います。

EzXML.jlを作る前に、すでにLightXML.jlというパッケージがJulia界にはあったんですね。だけど、いくつか不満がありまして。

そもそも、メモリ管理を自前でやる必要がありました。freeっていう関数があって、それを呼ばないと、メモリがキープされたまま残り続けるという、ちょっとアレな感じのパッケージだった。「これは面倒くさいだろう」と思って、自分で新しいものを作ったんですね。

そもそもどういうものかというと、(これは)XMLを扱うんですけど、XMLを扱うにはlibxml2という標準的なCで書かれたライブラリがあって、それをラップするかたちでこのEzXML.jlもLightXML.jlもできてるんです。なんですが、Cのラップはちょっと難しい部分があるんです。

とくに、XMLは木構造になってるので、非構造のあるノードとあるノードをJuliaでラップしたときに、関係があるのに片方を勝手にGCが回収してしまったりすると、すごくメモリのエラーが起きたりします。

単純に、例えばJuliaのGCをするときに、勝手にCのほうの構造体も一緒に解放するようにすると、まだ使うXMLの木の部分が、勝手にJuliaのGCと一緒に消えちゃったりして、メモリがグチャグチャになってしまったりします。

こういう感じでけっこう難しいんですけど、(EzXML.jlでは)そのへんをがんばって解決しました。このへん、すごく話せるので、興味がある方は後で聞きに来てください。

あと、メモリ管理以外にも、XMLには名前空間という概念があって、(LightXML.jlは)それもサポートしていなかった。XPathもサポートしてなかった。巨大なXMLファイルを読むときに、一気にメモリに読んでしまうからストリーム処理をしたいけど、ストリーム処理もサポートしてなかった。あと、なんかインターフェースが変だった。

もうたくさん不満があって、これを騙し騙し使うよりは、自分で新しいものを作ったほうがいいだろうということで、EzXML.jlというパッケージを作りました。

特徴としては、先ほどいくつか挙げた問題を全部解決するように作っていて、「霊長類のための簡単なXMLとかHTMLを処理するためのパッケージ」というコンセプトになっています。

XMLやHTMLを読み込んだり、トラバース、木構造になってるんで、それを、木構造、あっちこっち、DOMインターフェースでいったり。XPathを使って要素を検索したり、名前空間(namespace)をちゃんと扱ってくれたり、エラーメッセージがそのへんにピョーンと出ないで、うまいことJuliaのI/Oのシステムにいれたり。

あとは、メモリマネジメントを勝手にやってくれる。ここが一番大変でした。他にもドキュメントのバリデーションをしてくれたり、大きなXMLを一気にメモリに読まないで、段階的に読む、ストリーミングパーシングができるようにしています。

インターフェースを比較する

インターフェースを簡単に比較してみると(スライドを指して)こんな感じです。

ほぼ同じことをやってるんですが、(EzXML.jl では)using EzXMLをして、ex1.xmlというファイルがあったら、それをreadxmlという関数で読み込みます。これは一気にメモリに読み込む、ドキュメントとして読み込む感じですね。

LightXML.jlだと、parse_fileっていう、何をするのかよくわからない関数名になっていたので「ちょっとこれは……」と思って、readxmlというわかりやすい名前にしています。

(EzXML.jlでは)そのドキュメントを取ってきたら、XMLでルートノードを取ってくるんですけど、そのときに.rootと書けるようにしています。こっち(LightXML.jl)ではrootという関数を使わないといけないので、このrootっていう変数名を使えなくて、xrootとか、適当な名前を付けなきゃいけない。

(EzXML.jlでは)rootの以下にあるノードを上から順に観察するとき、eachnodeっていう関数を使って、ルートノードからイテレータを使って1個ずつ子供のノードを取り出せるようにしています。こっち(LightXML.jl)はchild_nodeとなっていて、なぜかアンダーバーが入ってたりして、インターフェースがちょっとイケてない。

あと、EzXML.jlでは、タイプを取ったり名前を取ったりするとき、Juliaっぽく書けるようになっています。LightXML.jlだと、XMLElementというものにいったん変換しないとXMLの要素が使えないんですが、そういったことがないようになっています。

こういうふうにインターフェースを向上させて、豊富な機能も備えています。

先ほども言いましたけど、自動のメモリ管理、これは本当に重要です。加えて、名前空間、XPath、ストリーミング処理もサポートしています。

これ、どういうところに必要かっていうと、バイオ系のデータファイルで、Gene Ontologyとか、もし知ってる人がいたらわかると思うんですけど、非常に巨大なバイオ系のターム、専門用語を、Ontologyというデータ構造みたいなものでまとめて扱えるというものがあるんです。そのデータファイルが非常に巨大で。

名前空間を使っていて、XPathも必要で、ストリーミング処理も必要でという、すべての機能を使わないとうまくパースできないという事情があったんですが、EzXML.jlでは解決して使えるようになりました。このへんをラップして、別のバイオ系のパッケージを作ってたりします。

TranscodingStreams.jl

もう1つ、TranscodingStreams.jlを紹介します。これもJuliaのパッケージで、そもそも「gzipをいい感じに扱いたかった」というモチベーションでした。

gzipはみなさん、ふだんファイル圧縮形式としてデフォルトで使われてると思います。これ(を扱うパッケージ)はもちろん私が作る前からあって、そのままの名前のGzip.jlというものと、あとは、gzipを扱うときはlibzというライブラリを使うのが標準的なので、それをラップしたLibz.jlというものがありました。

実は私、Libz.jlの開発にも関わってるんですけど、私がデザインしたものではないので、ちょっと不満があったんですね。

Gzip.jlは非常に遅い。Cのライブラリでligzのライブラリを呼んでるんですが、インターフェースのラップの仕方があんまり効率的じゃない。ストリーム処理がうまくできないような構造になっていて、処理が何十倍も遅かったりしたんです。

それを解決するために、別の人がLigz.jlを作ったんですが、これは確かに速いんですけど、デザイン的にちょっと拡張しにくくて、gzip以外には基本的に使いづらいデザインになっています。

そこで、gzip以外にもいろいろな圧縮解凍フォーマットを同じインターフェースで扱えるように新しく作ったのが、このTranscodingStreams.jlです。

例えば、Facebookのzstdという圧縮解凍方式が去年あたりに出てきて、gzipより高速で高圧縮っていう、完全にgzipの上位互換みたいな、すごくいい圧縮解凍形式、ライブラリがあります。

他には、標準的に使われているxz、昔はlzmaとか呼ばれてましたけど、あとはbzip2、これはたぶん最近は使う方もかなり少なくなったと思いますが、こういう他の形式もあるということで、これらを全部一緒くたに使えるようにするのがTranscodingStreams.jlです。

TranscodingStreams.jlの特徴

特徴としては、このTranscodingStreams.jlというパッケージのなかに、TranscodingStreamという型が1個だけ定義されています。これがメインの型になっていて、JuliaのIOという抽象型の、継承ではないですけど、下位の型です。

このTranscodingStreamは、2つ、型パラメータを取ります。1つがCで書かれているdata transforming codecで、データを変換するための型です。もう1つが(スライドを指して)ここでSと書かれているラップされる型ですね。

これは何をしてるかというと、Sという、もともとあるIOのオブジェクトを、データ変換をかませて、さらにIOにする。IOをCodecで変換して、またIOにするということをしているような内容になります。

謳い文句としては、オーバーヘッドをできるだけ減らすために、バッファリングとか、いろいろなスペシャライズドなソフトを定義しています。あとは、インターフェースが非常にconsistentで、普通のIOが使える人だったら、これも簡単に使えます。

(スライドに)Genericと書いてあるのは、例えばファイルを扱うとか、インメモリでバッファを扱うとか、パイプを扱うとか、いろいろなIOが全部統一的に扱えるということになってますね。これでパラメータ化されている。

あとはextensibleで、これが重要なんですが、このCodecという部分を取り替えるだけで、例えばgzipだったり、zstd、xz、lzma、bzip2、いろんなデータ変換の方式をここに埋め込むことで、データの変換を行えるようになっています。

このファミリーにはいくつかこれに依存してるパッケージがあって、先ほど言ったように、全部Codec、zlibとか、zstdとかがあります。

全部Codecというプレミックスで始まるパッケージで、このへんを開発しています。例えばbase64とかも扱えますね。

あとは、私が作ったものではないですが、lz4という、圧縮率は劣るけど高速な圧縮解凍形式を、Inveniaというカナダの会社が作ってくれたりしています。同じインターフェースを継承してファミリーに加わってくれたということですね。

こんな感じで、新しい形式のサポートが非常に簡単なので、外部の人でもCodecを1個定義すれば、簡単にそれがIOとして使えるようになると思います。実装すべき型、さっき言ったCodecと、メソッドをいくつか定義するだけなので、非常に簡単です。