2009年5月の小ネタ
TeX 使うの嫌になったけど他に選択肢もないので Ruby で頑張ることにした
事の起こりは、中川義行さんが配布されている「研究者専用履歴書・業績書スタイルファイル」を使って作ってあった履歴書の更新をしていた時のこと。 業績欄のところで一部、文字が枠からはみ出してしまう箇所があったため、 文字サイズを若干小さくすることで対処しようとしたら、
! TeX capacity exceeded, sorry [parameter stack size=6000].
なんてエラーメッセージが出てしまった。たかだか "\small" を付け加えただけなのだが、 それで内部スタックを使い尽してしまうことなんてあるのだろうか。
という訳で、他にやらなければならない仕事は沢山あるにも関わらず、 嬉々として TeX マクロのハックを始めたのだが、原因を突き止めた頃には心底疲れ果ててしまった。
原因は TeX のマクロ展開の罠とでも言うべき点で、端的には
\documentclass{article} \def\name{Foo} \edef\smallname{\small\name}
これだけで前述のエラーを再現させることができる。 要は、\edef の中で \small というマクロの展開をしてしまっていることが問題となっている。
ただこれだけなら、\small の前に \noexpand をつければいい、ということになるのだけど、 おおもとのスタイルファイルは、枠線だらけの伝統的な履歴書の体裁を作り出すべくちょっと普通の LaTeX スタイルファイルとは異なる実装になっており、 内部で複雑なリスト処理をしていたりするのでマクロの展開について正しい解決方法を探るのが非常に難しくなっている。
普通の LaTeX 文書は、テキスト部分はおおむね書かれた通りの順番で処理が進んでいき、 途中に現れるマクロ呼び出しはその都度その場所に展開されるという手順に沿う。 しかし問題のスタイルファイルは TeX ファイルの中ではマクロ変数へ名前や学歴などのデータを片端から代入していき、 それが終わったらスタイルファイル側でそれらのマクロ変数を、 picture 環境の中をあっち行ったりこっち行ったりしながら展開しては貼り付けていく、 という構造になっている。
この構造は TeX の流儀から行くとかなり面倒なもので、実際マクロ展開で色々と苦労しているのだが、 プログラミング言語やデータベースなどの扱いの心得がある者から見れば、 わりあい理解のしやすいものであるし、文書の見通しもずっと良くなる。 他のプログラムとの連携もやりやすい。ここで TeX が好きで好きでたまらないという人であれば、 より頑健なマクロの書き方を調べていき、TeX の中で何とかするのであろうが、 僕の場合は単純なループ処理や四則演算ですら超絶技巧を必要とする TeX マクロの世界にほとほと嫌気がさし、 ついには、そもそも履歴書なんて構造が複雑な訳でもなし文章量が多い訳でもなし、 OpenOffice.org なり何なり、WYSIWYG ツールを使って書いた方が圧倒的に速いんじゃなかったのかという根本的なことに気がついてしまった。がしかし、 どうせまたそっちはそっちで四苦八苦することは目に見えているので、 今後の事も考え、いかに TeX マクロ世界に足を踏み入れることなく TeX 文書の自動生成をするかについての、いい演習材料だと思ってみることにした。
問題の根本は、TeX を使ってプログラミングすることの阿呆みたいな難しさにある。 だいたいが、TeX マクロは基本的に文字列置換以上の機能を持たないところに無理矢理プログラミング言語としての機能を持たせているため、 一般的なプログラミング言語、それも現代的なかゆいところに手が届く楽々プログラミング環境に慣れた者にとっては TeX プログラミングなんて阿呆らしいことこの上ない。デバッグ支援機能もたいしてある訳でなく、 バックスラッシュにまみれた見にくいコードを相手にするくらいなら、 慣れた言語で書けた方が絶対良いに決まっている。
という訳で、Ruby と ERB を使って、TeX 文書に Ruby プログラムを埋め込む形に書き直してみたら、 これが楽なこと楽なこと。例えば学歴は "[[年,月,内容],...]" という配列に入れておき、
% @education.each {|e| \putlist{<%=e[0]%>}{<%=e[1]%>}{<%=e[2]%>} % }
といった感じで展開できる。これを TeX マクロで書こうとすると、 二次元配列の操作だけで手間がかかるしバグを呼びがちだ。 このやり方だと二次元配列に格納したデータを表形式にするとか、 カウンタの計算なんてのもあっという間。リストの長さの計算なんてのも、 いちいちリストを辿って数えるなんて馬鹿な工夫は必要ない。
こうやって久しぶりに不自由な環境でプログラムを書いてみると、 やっぱりプログラミング言語の優劣ってのは、 確実にあるんだなぁ。TeX はチューリング等価だといっても、 「プログラムがどうにか書ける」と「プログラムが楽に書ける」の差は大きい。 \noexpand とか \expandafter といった、 マクロ展開のタイミングをずらすために用意された命令の使い方に頭を悩ました後は、 それを強く実感できる。
論文を書く前につい TeX マクロをいじってしまうという症状について
さぁ論文書くぞ、という時に決まってまずスタイルファイルの整備など、 TeX マクロいじりに精を出すという症状が業界関係者に広く見られる。 これについてのちょっとした冗談。
「なんで The Art of Computer Programming はなかなか続刊が出ないの?」
「Knuth がまだ本のための TeX マクロをいじっているからさ」
libqrencode-3.1.0rc1 を公開
libqrencode のバージョン 3.1.0 リリース候補 1 を公開します。 性能がだいぶ向上しました。中身にかなり手が入ってますが、API に変更はなく、 バイナリ互換になっている筈です。
今回のこのバージョンアップは、頼まれもしないのにエンコード速度向上とバグ取りに精出したもの。 速度向上はソフトウェアの使命の一つ、とはいうものの別に今までそんなに無茶苦茶遅かった訳でもないし、 そもそも QR コードの出力なんてミリ秒単位でしのぎを削らなきゃならないようなもんでもないので、 ある意味ほとんど意味のないブラッシュアップ。企業でこんなことやったらむしろ怒られるくらいだ。
バグ取りについてはもっとひどくて、今回やったのは malloc() の戻り値のチェックを厳密にやるというもの。 つまりメモリが足りなくなっているような状況でも Segmentation fault で落ちずにちゃんとエラーをアプリケーションまで返すようにする、ということ。 これを真面目に実装しようとすると非常に面倒なわりに、 今どき滅多なことではメモリ不足で落ちるなんて状況にはならないし、 そんな状況で正しくエラーをアプリケーションに返したところで、そこからアプリケーションが正常に走り続けられるというものでもないので、実益の乏しいこと甚しい。 ライブラリは勝手に落ちないのが正義であるのはもっともだけど、 libqrencode の使われ方を考えると、コマンドラインでの利用にしても CGI 用プログラムにしても、 一般的には一つのプロセスとして動き続けるというものではないから、 malloc() の戻り値が NULL だったらその場で exit や abort したところでまったく問題ない。 にも関わらず今回はどんなに深いところでメモリ不足に陥いっても、 ひたすらさかのぼってエラーを返すように (しかも不要なヒープは極力 free してメモリリークを回避) したのは、単に自分の根性試しに他ならない。
ところで、その根性試しの過程での教訓として、「malloc はできれば避ける」 べきことがわかった。というのも、万一 malloc が失敗した場合、 例外処理もガベージコレクタも持たぬ C プログラムでは、 それまで一時的にヒープから確保していたメモリは逐一開放してから関数を抜けねばならない。 その面倒を避けたければ、malloc の使用を控えるしかない。といっても、 マルチスレッド対応などを考えればそう簡単に避けられるものではないのだが、 多値の受け渡しのためだけに気前よくヒープメモリを使うような (Perl や Ruby で多値の戻り値を配列にして渡しているような) お大尽な使い方は、可読性をあまり損わずに避けることができた。 といってもメモリの確保場所がヒープからスタックに移っただけなのではあるが、 速度向上が期待できるし、メモリの断片化も避けられる。
まぁ、そんなのはかつては当たり前のようにやっていたんだけど、 これまでの libqrencode での隠れたテーマが 「まるでよくできたスクリプト言語であるかのように、湯水のようにヒープを使ってみよう」だったので、 あらためて常識的なスタイルが妥当なものだったことを再認識した次第。 ようやっと、malloc 多用と速度・メモリ効率重視のスタイルの間のバランスの勘所がわかってきた感じがする。
ちなみに、libqrencode に残っている問題としては、linked list を多用しているためメモリ断片化が進行しやすいという点ではあるのだが、 さすがにこれに自分で手をつけるのは面倒なので、 GLib みたいな既存ライブラリを使うか、放置しておくことになる。 実用面から考えれば、圧倒的に後者。