🍊

なぜあなた(ぼく)はTeXでプログラミングできないのか

2021/12/25に公開

TeX & LaTeX Advent Calendar 2021の25日めの記事です。昨日は @golden_lucky による「TeXで使うプログラミング言語まとめ」でした。)

(本稿ではTeXとLaTeXを区別しないでTeXと書く。つまり、TeXとLaTeXに区別が必要ない話をする。)

人がTeXのプログラミングに手を染めるのは、だいたい次のいずれかの状況だろう。

  • 見た目が気に入らない
  • 文字列を機械的に制御したい

そもそもあなた(ぼく)がTeXでやっているのは「文書を書く」なんだから、こういう問題に直面したときに「プログラミングで解決しよう」ってなることには飛躍があるんだが、コンピューターでドキュメントを書いていると、どこからともなく「このドキュメントがいい感じじゃないのはプログラミングが足りていないからだ」という声が聞こえてくる。見た目や文字列処理は機械的に処理されるべきものであるはずなのに、こうやって文章として手打ちをしているのは、プログラマ的な怠惰の精神にもとるというほかない。文章を書き始める前に、これから書くものについて見た目や文字列のパターンを抽象化し、それをコード化しておくべきではないだろうか。

もちろんこれは虚言であり、ほんとうはまっさきに文章を書くべきである。見た目や文字列の機械的な処理はあとで考えるべきだ。そもそも文章というのは書いて終わりではなく、ましてや書き始める前に最終形が見えているものではなく、書いたものを何度も読み返して修正していくことではじめて形になるのだから、抽象化が先に可能だと考えるのが間違っている。

それでも、抽象化したい。ある程度まで書ければ見た目も気になってくるし、機械的に制御できそうな文字列のパターンも見えてくる。よろしいならば抽象化だ。さいわいTeXには、\ifとか\defとか、いかにも「プログラムが書けそう」な制御つづりが備わっている。これで便利なプログラムを書いたほうがやっぱり執筆も捗るのでは??

たとえば英文の翻訳をしていて、「カンマとandによる名詞の列挙」のパターンがすごくすごくたくさん出てくることに気づいたとしよう。これをすべて日本語の読点と「および」に書き換える必要があるとする。「Leo, Anderson and the One」なら「Leo、Andersonおよびthe One」という具合だ。同じパターンをエディタでちこちこ直すのには飽き飽きしてきたし、そもそもこういうのってミスを防ぐためにもプログラムを書いて統一的に扱うべきなんじゃない? よし、ひとつTeXのマクロを書いてやろう。

ちょっとウェブで「TeX マクロ 置換」とか調べたプログラミング経験者なら、こんなのがすぐに書けるかもしれない。

\def\replacecomma#1, #2{#1、\replaceand#2}
\def\replaceand#1 and #2{#1および#2}

やった、うごくぞ。

\replacecomma Leo, Anderson and the One
% => Leo、Andersonおよびthe One

もちろん、これは列挙される数が3つ以外では破綻する。はっきりいって使えない。使えないんだけど、これを\replacecommaThreeとかいう名前にして、引数の数ごとに\replacecommaTwoとか\replacecommaFourみたいなマクロ群を作って原稿で使ってしまう人、意外にいるみたいなんですよ。単語を置き換えるだけのマクロを翻訳メモリ的に使おうという発想は遅かれ早かれ破綻するのでやめてください。

それはそうとして、じゃあ「ちょっと自分のドキュメントで使うぶんには困らない」程度のやつを作るとしたら、どうすればいいか。これが最適かどうかはわからないけど、列挙が2つでも4つでもいけるやつとなると、やはりこれくらいには「ぱっと見、わけがわかんない」マクロを書くしかない気がする。

\def\@endcomma{}
\def\@comma#1,#2\@endcomma{%
  \ifx\relax#2\@and #1\else #1、\@comma #2\@endcomma\fi}
\def\@and#1 and #2{#1 および #2}
\DeclareRobustCommand{\replaceandcomma}[1]{%
  \@comma #1,\relax\@endcomma}

ただ、実用的なものとなると、これでもぜんぜん足りない。列挙が1つしかない場合や最後の2つの引数の間にandがない場合にはエラーになってしまうからだ。お馴染み、どこで何のエラーが起きているかさっぱり読み取れないTeXのエラー。そういうケースで「このマクロ」から警告を出してあげるようにしようと思ったら、こんな感じに手当てをする必要がある。

\def\@endcomma{}
\def\@comma#1,#2\@endcomma{%
  \ifx\relax#2\@and #1 and \relax
  \else#1、\@comma #2\@endcomma\fi}
\def\@and#1 and #2{%
  \ifx\relax#2%
    #1\@latex@warning{There's no 'and' or enough items.}
  \else
  #1 および \@@and #2%
  \fi}
\def\@@and #1and{#1}
\newcommand{\replaceandcomma}[1]{%
  \@comma #1,\relax\@endcomma}

しかし、この例のように\@latex@warningを使うと副作用があるので、これだと使い方がやや限定されるようになる。具体的には、たとえば\edefの中では使えなくなる。

…。

っていうか、「\edefってなんだよ」って感じですよね。「そんなマイナーなものすっとばして簡単な例でマクロの書き方だけさくっと教えろよ、なんでTeXの解説はそうやってすぐに細かいこと言い出すんだよ」と思うのが自然ですよね。

でも、こういうのに言及しておかないと「ハマらない」マクロの書き方が説明できないんですよ。だから言及するわけだが、その結果やたらにスノッビーな解説になってしまう。スノッビーな解説は誰も読みたくないので、ツイッターなどでは「TeXのマクロの書き方のわかりやすい入門記事がネットにない」という愚痴が定期的に観測されることになる。

とりあえず、列挙が1つだとエラーになるバージョンに話を戻そう。

\def\@endcomma{}
\def\@comma#1,#2\@endcomma{%
  \ifx\relax#2\@and #1\else #1、\@comma #2\@endcomma\fi}
\def\@and#1 and #2{#1 および #2}
\DeclareRobustCommand{\replaceandcomma}[1]{%
  \@comma #1,\relax\@endcomma}

なにをどう考えたら、こういうごちゃごちゃしたマクロの定義が生まれるのか?

まあ、このマクロは自分で書いたわけだけど、おそらくこんな心の流れで書いた。

  1. 引数に対するパターンマッチでやれそう
  2. しかし、単一のパターンじゃないので、1つのマクロではパターンを網羅できないぞ
  3. 下請けマクロで再帰処理する必要がある
  4. そのために終端のマークを用意しよう → とりあえず\@endcommaを定義する
  5. まずはandは忘れてコンマ区切りのパターンマッチだな
  6. \def\@comma#1,#2\@endcomma{...の定義を考えはじめる
  7. \@endcommaを見つけるまで再帰、という基本方針は上記の3と4の時点で思い描いているので、それを書こうとする
  8. このへんでようやく、「最後のA and Bをどうやってキャッチするか」を決めないといけないことを思い出す
  9. 最後がA and B\hogehogeみたいな形になってれば、「再帰したときに#2\hogehogeかどうか」を\ifxで調べてキャッチできそう
  10. そのとき用の下請けマクロを書き出す(\def\@and#1 and #2{...
  11. あとは\hogehogeも準備しないといけないけど、もう面倒だから\relaxでいいか

ここまでの試行錯誤で、だいたいこんな感じの定義ができあがる。

\def\@endcomma{}
\def\@comma#1,#2\@endcomma{%
  \ifx\relax#2\@and #1\else #1、\@comma #2\@endcomma\fi}
\def\@and#1 and #2{#1 および #2}
\def\replaceandcomma#1{%
  \@comma #1\relax\@endcomma}

さっそくためしてみよう。だがこれはエラーになる。

\replaceandcomma{Leo, Anderson and the One}
% => 
% the One\relax \@endcomma \fi \fi  \end {document} 
% ! File ended while scanning use of \@comma.
% <inserted text> 
% \par 
% <*> main.tex

なにがまずいのか。

エラーメッセージをよく読むと、\@commaの引数をスキャンしている最中にファイルが終わっちまった、と言っているので、\@commaの引数の定義を見返す。もちろん、自分が書いた定義をぼーっと見返しても何もわからない。しかたがないので、頭の中もしくは紙の上で、「この再帰が実際の引数に対してどう展開されるか」をなぞっていく。

すると、だんだん、何がまずかったのかわかってくる。というか、そうやって地道に自分が書いたマクロがどうやって展開されていくかを考えないと、なにがまずかったのかまずもってわからない(少なくともぼくには)。

この場合の答えを言ってしまうと、\@commaが期待する引数のパターンにはカンマが含まれているのに、再帰の最後のAnderson and the Oneにはカンマが含まれていないことでエラーになっている。これを解決するには、最後にカンマが残るようにしてあげればいい。つまり、初回に\@commaに引数を渡すときにカンマをつけてあげればいい。

\def\@endcomma{}
\def\@comma#1,#2\@endcomma{%
  \ifx\relax#2\@and #1\else #1、\@comma #2\@endcomma\fi}
\def\@and#1 and #2{#1 および #2}
\def\replaceandcomma#1{%
  \@comma #1,\relax\@endcomma}

最後の行を変更した。これは、少なくともこの例では意図していたように動く。

\replaceandcomma{Leo, Anderson and the One}
% => Leo、Andersonおよびthe One1

TeXでマクロを書くとき、ぼくはだいたいこんな感じに試行錯誤しながら「それっぽい」ものへとたどり着いている。この記事では、あえてそういうのがなくてもとりあえずのゴールにはたどり着ける例を選んだわけだけど、現実の問題では、展開制御(いわゆる\expandafter)や代入(\letとか)を使わないと意図した挙動にたどり着けない問題も多い。

さらに問題を難しくするのは、あなたが書いたTeXマクロは「ぜんぜん別の誰かが書いたTeXマクロと一緒に使いたくなることが多々ある」ということだ。とくに、latex.ltxというファイルで定義されているTeXマクロたちは、ふつうに文書を書くときは絶対に避けられない。定義してみたマクロが、それらと組み合わせたときに変な挙動をしているようだと、そっちのTeXマクロについてもlatex.ltxの中から定義を探し出して「引数に対してどういう展開が起きるか」を頭の中もしくは紙の上で検証しないとトラブルシューティングができない。先に\edefとかの話をしたけど、そういうのも説明しておかないといけないと言ったのは、こういう事情がある。

おどおどしいことを書いてきたけれど、結論は2つです。

  • TeXマクロを書くな(ここに書いたようなことが面倒な場合)
  • TeXマクロを書け(ここに書いたようなことが面倒でない場合)

面倒かどうかは、気持ちの問題でもあるけど、スキルの問題でもある。世の中のすごいTeXプログラマーたちは、きっと、ぼくがここで書いたようなぐだぐだした試行錯誤ではTeXマクロを書いていないだろう。たくさん読んでたくさん書ければ、もっとイディオマチックに、しゅっとマクロが書けるようになるのだと思う。

いずれにしても、ふんわりした気持ちで面倒を減らそうと思ってTeXでプログラミングをはじめると間違いなく面倒は増える、とだけは言えるんじゃないでしょうか。ぼくはTeXマクロをあまりしゅっとは書けないけれど、なんだかこういう面倒がたまらなく楽しいってなるときがあるので、そういうときにTeXマクロを書いています。

Discussion