この記事は TeX & LaTeX Advent Calendar 2021 の(勝手にやった)26日目の記事です。25日目は golden_lucky さんでした。その後なんと27日目,28日目,29日目,30日目,31日目と続きました。
昨日,TeX & LaTeX Advent Calendar 2021 が無事に完走しましたね。golden_lucky さんによる24~25日目のTeX上でのプログラミングに関する話がとても面白かった(TeXマクロを書くときの自分の心理の動きが見透かされたように明文化されていて刺さりました)ので,自分もTeX言語でのプログラミングの一例を挙げてみることにしました。
先日 TeX Q&A で「絶対値記号の高さの調整について」というトピックが立っていました。要するに,\left
~ \right
によって括弧を付けると上下対称に括弧が伸びて間延びする場合があるので,「括弧の中央が数式の中央ラインからずれてもよいから必要最低限の長さの括弧で囲ってほしい」という要望です。
【\left
~ \right
を使った場合の組版結果】
【要望された組版結果】
このような組版が数式組版として適切かという議論はさておき,TeX言語プログラミングの練習として,このような組版をマクロで自動的に実現するための方法を考えてみましょう。
絶対値記号をこのように出力するための考え方とコードが,しっぽ愛好家さんの投稿に示されています。「絶対値の中身を一度ボックスに保存しておき,それを縦方向中央に移動させ,それを \left| ~ \right|
で上下対称に囲み,再び移動量の分だけ元の高さに戻す,というわけです。
ここに提示されたマクロは,\norm{中身}
という形で「中身」を絶対値記号で囲む命令となっていますが,これを
- 絶対値に限らず様々な括弧類に汎用的に対応させる
\left
~\right
と同様の構文で使えるようにする
ように拡張してみましょう。
ここでは,\autoleft
~ \autoright
という名前で,\left
~ \right
と同様の構文で使えるような命令を用意してみます。
コード例
\documentclass{article} \usepackage{amsmath} \usepackage{xstring} \makeatletter \newcount\autobracket@nest \def\autoleft#1{% \autobracket@nest=\z@ \def\autobracket@l{#1}% \def\autobracket@content{}% \autobracket@content@save } \def\autobracket@content@save#1\autoright#2{% %%%% #1 の中にいくつ \autoleft, \autoright があるかをカウントして,その個数差の分だけ後でスキップする \edef\@tempa{\detokenize{#1}}% \edef\@tempb{\detokenize{\autoleft}}% \StrCount{\@tempa}{\@tempb}[\@tempc]% \advance\autobracket@nest\@tempc\relax \edef\@tempb{\detokenize{\autoright}}% \StrCount{\@tempa}{\@tempb}[\@tempc]% \advance\autobracket@nest-\@tempc\relax \ifnum\autobracket@nest=\z@ %% #1 に余分な \autoleft が含まれないときは終結 \expandafter\def\expandafter\autobracket@content\expandafter{\autobracket@content#1}% \def\autobracket@content@save@next{\autobracket@autoright#2}% \else %% #1 に \autoleft が余分に含まれるときはカウンタ値を減らして前に進む \advance\autobracket@nest\m@ne \expandafter\def\expandafter\autobracket@content\expandafter{\autobracket@content#1\autoright#2}% \let\autobracket@content@save@next\autobracket@content@save \fi \autobracket@content@save@next } \def\autobracket@autoright#1{% \def\autobracket@r{#1}% \autobracket@save@counters {\mathpalette\autobracket@enclose{\autobracket@content}}% } \newbox\autobracket@body \def\autobracket@enclose#1#2{% \hbox{% \m@th\autobracket@restore@counters \setbox\autobracket@body\hbox{$#1#2$}%% 中身を保存 \dimen@ii\dp\autobracket@body%% 中身の底の位置を保存 \setbox\autobracket@body\hbox{$\vcenter{\box\autobracket@body}$}% %% ↑中身の天地中央の位置が「数式の軸」のところになるよう置き直す \advance\dimen@ii-\dp\autobracket@body%% 置き直した際の中身の移動量を計算 \lower\dimen@ii\hbox{$\left\autobracket@l\box\autobracket@body\right\autobracket@r$}% %% ↑括弧を付けた後,上下方向について元の位置に置き直す }% } \let\autobracket@restore@counters\relax \def\autobracket@save@counters{% \def\@elt##1{\noexpand\@nameuse{c@##1}\number\@nameuse{c@##1}\relax}% \edef\autobracket@restore@counters{\cl@@ckpt}% } \makeatother \begin{document} %% テスト \[ x + \autoleft|\overrightarrow{AB}\autoright|^2 + \autoleft|\overline{z^{n^2}}\autoright|^2 + \autoleft(\dfrac{\dfrac{x+1}{x^2}}{x^2+x+1}\autoright)^n + \autoleft[\dfrac{x^2+x+1}{\dfrac{x+1}{x}}\autoright]_0^1 + \autoleft\{\dfrac{\displaystyle\sum_{k=1}^n a_k}{n}\autoright\}_{n=1,2,\ldots} \] %% ネストするケースのテスト \[ \autoleft|\dfrac{\dfrac{x}{2}}{x+1}+4\autoleft(\dfrac{\dfrac{x}{2}}{x+1}\autoright)\autoright| +\autoleft|\dfrac{2\autoleft(e^x+\dfrac{x}{2}\autoright)}{x^2-x}\autoright| \] \end{document}
出力結果
実装のアイデア
アイデアとしては,
\autoleft
が現れたら\autoright
までをパターンマッチで取得- 左右の括弧の種類を
\autobracket@l
,\autobracket@r
として保存しておく - 囲まれた内容を保存し,それをしっぽ愛好家さんの
\norm
と同様に定義された\autobracket@enclose
の中で使用する
というものです。ただし,単純に \def\autoleft#1#2\autoright#3
といった形でパターンマッチさせてしまうと,ネストして使用している場合に対応できません。#2
の部分が最短でマッチされ,個数がつり合うところの \autoright
がマッチしないからです。
そこで,上記コードでは,パターンマッチしたものを即座に採用するわけではなく,中身をチェックしています。\def\autobracket@content@save#1\autoright#2
において,#1
にマッチしたトークン列の中に \autoleft
と \autoright
いうトークンが含まれる個数の差をカウントする*1ことで「現在ネストの何重目にいるか」を判断し,その個数だけ \autoright
を超えて先の \autoright
まで繰り返しマッチさせていきます。その過程でスキップしたトークン列を追記しながら保持しておき,トップレベルの \autoright
が閉じた瞬間,\autobracket@enclose
にその中身と括弧の種類を引き渡す,という処理を行っています。これが再帰的に実行されることで,ネストされた \autoleft
~ \autoright
に対応できる,というわけです。
「トークン列の中に \autoleft
, \autoright
というトークンが何個含まれるか」を数えるには,xstring パッケージ の \StrCount
というコマンドを用いています。#1
と \autoleft
, \autoright
をともに \detokenize
で \the
-文字列化しておいた上で,含まれる個数をカウントしています(カテゴリーコードまで一致しなければ一致判定されない点に注意)。
……というわけで,解説を始めると,どうしてもgolden_luckyさんの25日目の記事にある
「そんなマイナーなものすっとばして簡単な例でマクロの書き方だけさくっと教えろよ、なんでTeXの解説はそうやってすぐに細かいこと言い出すんだよ」と思うのが自然ですよね。
でも、こういうのに言及しておかないと「ハマらない」マクロの書き方が説明できないんですよ。だから言及するわけだが、その結果やたらにスノッビーな解説になってしまう。
のような解説の典型例になってしまいましたね😓。実際,このマクロを書くときの自分は,まさにこの記事にある
- 引数に対するパターンマッチでやれそう
- しかし、単一のパターンじゃないので、1つのマクロではパターンを網羅できないぞ
- 下請けマクロで再帰処理する必要がある
- そのために終端のマークを用意しよう → とりあえず
\@endcomma
を定義する- まずは
and
は忘れてコンマ区切りのパターンマッチだな\def\@comma#1,#2\@endcomma{...
の定義を考えはじめる\@endcomma
を見つけるまで再帰、という基本方針は上記の3と4の時点で思い描いているので、それを書こうとする- このへんでようやく、「最後の
A and B
をどうやってキャッチするか」を決めないといけないことを思い出す- 最後が
A and B\hogehoge
みたいな形になってれば、「再帰したときに#2
が\hogehoge
かどうか」を\ifx
で調べてキャッチできそう- そのとき用の下請けマクロを書き出す(
\def\@and#1 and #2{...
)- あとは
\hogehoge
も準備しないといけないけど、もう面倒だから\relax
でいいか
というのと同様の試行錯誤をしました。みんな同じような心理でTeX言語プログラミングしているんだなぁと知って,ちょっと安心した次第です。
というわけで,皆様,今年も大変有益な Advent Calendar,ありがとうございました!よいお年を!
*1:パターンマッチにおいてスキップされる,ブレース {} に囲まれた内側にバランスの取れた \autoleft と \autoright が存在することも想定して,個数の「差」をカウントしています。