variadic templateを用いいたライブラリについての雑感
年末年始にvariadic templateを用いたライブラリを遊びで作っていて、 これはちょっと面白いな、と思っての雑感。(C++でシンボリックなツリーを扱うライブラリとかは無いものか?)
tupleとかは良く使っているし、variadic templateを用いたテンプレートのutilityみたいなの(is_allなんちゃらとかそういう述語の実装)は作った事もあるが、 ある程度の再帰構造を持ったvariadic templateのクラスを実装したのは今回が初めてだ。 たとえば今回書いたのは以下みたいに使う奴。
using var_op = taccessor<test_sym::variable, string>;
using int_imm = taccessor<test_sym::int_imm, int64_t>;
using add_op = taccessor<test_sym::add, node, node>;
using sub_op = taccessor<test_sym::sub, node, node>;
// var_op=node1 in node2.
using let_op = taccessor<test_sym::let, var_op, node, node>;
let_opの中ではvar_opを参照していて、このvar_opはその上でユーザーが作ったもの。 こういう風にusingで定義したentityを別の所の構成要素に使える、 という類のライブラリの話を今回はしたい。
酷くて短いコード
variadic templateを使って目的を達成するのはある種のトリッキーさがある。 例えば位置を使って何かをする為に位置を埋め込んだ型を作ってそれを継承していき、キャストで位置の情報を取り出す、とか。 本来は並べた直積構造を取り出したいだけなのに、 実現したい事と実際に書くコードには結構ギャップがあって、 ある種のパズルを解かないといけない。 これはやりたい事をより直接に書ける最近の言語とは正反対だ。
コンパイルエラーを取るのも大変。 エラーは暗号的だし、自分の書いた書き方がダメな理由がコンパイラの実装的な制約であって、 本質的なロジックの間違いという訳では無いので、とても不毛な感じがする。 不要な事をやっている、というか。
書いたコードは非常に読みにくい。テンプレートのトリックの為に必要なクラスがいっぱい出てきて、 それらに抽象が無いのですごく低レベルな所でコードを読まないといけない。 これはSTLのコードを読む時などに皆が感じている事だとは思うが。
ただ、作ってみるとそこまで実装が多い訳でも無い。 今回遊びで作ったものは、variadic templateを使ったライブラリのコアの所は、 行数ではたったの140行だ。 行数の短さの割には時間がかかるし、可読性は低いけれど、 必要な時に実装することは出来ると思う。 意外と現実的な選択肢だな。
コンパイル出来るかどうかは試行錯誤で、理解度は低い。 でも書いたコードにバグがあるか、というと、コンパイルさえ通ればあまりバグは無い気もする。 結局シンタックスとか解決のルールとかいった要素が試行錯誤している本質的な理由であって、 そういう所に微妙なバグが入り込む事はあまり無い気がする。 基本的なケースが動けばちゃんと動く。
プリミティブと合成
この手のライブラリは、 基本的には何種類かのプリミティブをクラスとして実装して、 それの合成をvariadic templateで定義する訳だよな。 ライブラリではプリミティブと合成を定義し、使う側はそれらの合成を繰り返して目的の抽象を作り上げていく。 合成したものが新しいentityとなり、次の合成に使える、という所が特徴。
これはS式とかに似ているが、 プリミティブのクラスは処理を持っているので、 ある種の関数の合成でもある。 パーサーコンビネータに似ている、というかパーサーコンビネータもC++ならvariadic templateのライブラリの一種となるだろう。
プリミティブや合成は100行程度でも、それらを組み合わせて記述出来る範囲というのは、 そもそもプログラムという事自体が基本的な物を組み合わせて記述するものである事から考えても分かる通り、 非常に複雑な物が記述出来うる。
プリミティブと合成はその目的に応じてスクラッチから作る訳だから、 目的のための機能を持ったコードになる。 だからをそれを用いるユーザー側のコードというのは、 短い記述であっても、すごく多くの仕事をこなせる事がある。 そういうハマる用途では積極的に使っていく方がいいかもしれない。
variadic templateを用いたライブラリを書いて問題を解決する方法
通常の関数とかクラスで構成していくプログラムとは違った抽象化の仕組みが新しく導入されている訳だ。 それは意識的に用いないと使わない。どういう意識を持てば良いだろう?
こうした手法を使う為には、 プログラムのあるドメインを、 プリミティブと合成を定義する所とそれらを組みわせて問題を記述する所に分けて作る事になる。 つまりこの手のプログラムをする為には、
- 書いているプログラムの中から、この手法が適用出来る領域を見抜いて抜き出す
- プリミティブと合成を考えて実装する(variadic templateを用いたライブラリ)
- 合成を繰り返して問題を記述する
- この領域の外とのつなぎを書く
というような作業が必要になる。 4の所に都合が良いように1と2を考える、というのが自分で作る意義というか旨味と言える。
今回、仕事ではこの手法がばっちりハマる所を手実装で書いてしまった。 意外と見抜くのは難しいというか見過ごしがちだなぁ。 経験的には一年に一回くらいこの手のがバッチリはまる問題にあたっている気がする(前回はこれ>Pythonでトレーニングセット生成にコンビネータスタイルの言語内DSLを作る)。 日常的という程多くもないのだが、書く機会が無いというほど少なくも無い。
ばっちりハマる所では、しっかり見抜いて積極的に使えるようになりたいものだ。