マクロツイーター

はてダから移行した記事の表示が崩れてますが、そのうちに直せればいいのに(えっ)

TeX 者のための、“LaTeX の引数の規則”のキホン

ステマ


 
TeX & LaTeX Advent Calendar
 

2015/12/01 〜 2015/12/25
〜今さら人に聞けない、TeXのキホン〜

TeX & LaTeX アドベントカレンダー 2015

*  *  *

素敵な記事が連日届けられている TeX & LaTeX Advent Calendar の、初日のチョットアレな記事について、TeX 者のための補足の話をしたい。

といっても、例の記事の内容は LaTeX を前提にした話なので、plain TeX ユーザは無関係である。しかし、LaTeX パッケージ(や文書クラス)を作成している TeX on LaTeX な人には、特に注意していほしい箇所がある。

例の空白のアレ

これに対して、“引数の前”にある(字句解析で有効な)空白文字は無視されます。例えば次の例を考えてみましょう。

……(略)……

今と同様に次のソースの ␣ で示した空白文字も、引数を読み取る際には飛ばされます。(命令の直後の空白は字句解析で既に無視されています。)

\fcolorbox {red}␣{yellow}␣{注意}
\makebox [6zw]␣[s]␣{ヌバーン}
\qbezier (0,0)␣(40,0)␣(60,60)

ここで登場している \qbezier 命令((\qbezier 命令は picture 環境の中で使うもので、第 1 と第 3 の点を両端点、第 2 の点を制御点とする 2 次ベジェ曲線を描く。LaTeX カーネルでのオリジナルの定義は大量の点を並べて曲線を表現する非効率なものなので、これの使用は避けるべきである。代わりに pict2e パッケージを読み込めば、PostScript または PDF の曲線描画命令が利用されるようになる。pict2e 利用の場合、3 次ベジェ曲線を描く \cbezier 命令も使える。))は必須の引数として座標を 3 つ取る。*1全ての引数が必須であるので、\@ifnextchar 等が不要で 1 つのマクロで引数を全部受け取れそうである。\qbezier を実装する TeX マクロの引数書式はどうすればよいか。

\def\qbezier(#1,#2)(#3,#4)(#5,#6){%
  % 定義本体はダミー
  \typeout{start=(#1,#2); control=(#3,#4); end=(#5,#6)}%
}

一見これで何の問題もないように見えるが、実は大きな問題がある。これでは、例の記事中で \qbezier の例として挙げられている

\qbezier (0,0) (40,0) (60,60)

が通らずにエラーになってしまうのである。

Runaway argument?
0) (40,0) (60,60)
! File ended while scanning use of \qbezier.
<inserted text>
                \par

つまり、上のような定義では、引数列中の空白トークンは無視されないのである。

TeX の“引数の規則”をチョット攻略

TeX 言語のマクロの引数の読取ににおける空白の扱いについて注意が必要である。それは「区切り無し」の場合と「区切り付き」の場合で規則が異なるからである。具体的には、「区切り付きの引数の読取では空白トークンは読み飛ばされない」のである。

\def\macroA#1#2#3{\typeout{A2='#2'}}   % #2は"区切り無し"
\def\macroB#1#2[#3]{\typeout{B2='#2'}} % #2は"区切り付き"
\macroA a b{xyz}
\macroB a b[xyz]

このコードを実行すると、端末の出力は以下のようになる。

A2='b'
B2=' b'

\macroA\macroB の呼出で、#1a となるのはどちらも同じである。次に #2 を読み取る時に、区切り無しである \macroA の方では空白トークンが読み飛ばされる(従って、#2 は空白ではなく b になる)のに対し、区切り付きである \macroB の方では、空白の読み飛ばしが起こらない(だから a の直後から区切りの [ の直前までが #2 になる)のである。

この規則の類型として、先の \qbezier の書式中の )( のように区切りが連続している場合は、その部分は ) ( のように空白が入った部分にはマッチしない。

\def\macroC#1XY#2Z{\typeout{1='#1'}}
\macroC abcX YdefXYghiZ

この例の場合、#1abc ではなく abcX Ydef となる。

以上の話の結論として、「区切り付き引数を使った場合は、そのままでは『LaTeX の引数の規則』と合致しない動作を起こすので、何らかの対策が必要である」ということになる。

正しい \qbezier の引数書式

それでは、LaTeX カーネルでの \qbezier はどうしてこの問題を回避しているのか。\qbezier の定義は以下のような感じ((先に述べたように、実際の \qbezier の定義には先頭にオプション引数があるのでもう少し複雑である。))になっている。

\def\qbezier#1)#2(#3)#4({\@bezier#1)(#3)(}
\def\@bezier(#1,#2)(#3,#4)(#5,#6){%
  %......
}

つまり、2 ヶ所ある )…( の形になる部分で、括弧の間のトークンを捨てる前処理を行っているのである。従って、括弧の前に空白トークンがあっても正しく動作する。*2

\qbezier (0,0) (40,0) (60,60)
↓
\@bezier (0,0)(40,0)(60,60)
普通のオプション引数はどうか

LaTeX の命令のインタフェース”としての TeX マクロで区切り付き引数を使う場面で一番多いのは、やはり(普通の)オプション引数([...])の読取であろう。こちらについても、引数列中の空白について何か対策をする必要があるのだろうか。調べてみよう。

次のような書式の命令を考えてみよう。*3

% [BAR} はオプション引数で省略可能
\macroD{FOO}[BAR]{BAZ}

この引数書式の命令を実装するマクロは、典型的に以下のような実装になるだろう。

\def\macroD#1{% #1=FOO
  \@ifnextchar[%]
    {\xx@macroD@a{#1}}%
    {\xx@macroD@a{#1}[BAR-default]}% BARの既定値を渡す
}
\def\xx@macroD@a#1[#2]#3{% #1=FOO,#2=BAR,#3=BAZ
  % 処理本体......
}

この定義の下で、次のように「空白トークンを含む」呼出を行うとどうなるか。

\macroD {foo} [bar] {baz}

まず \macroD を展開すると以下のようになる。

\@ifnextchar[{...}{...} [bar] {baz}

通常、\@ifnextchar は下線部の直後にあるトークンを検査するわけであるが、今の場合、そこには空白トークンがある。この場合の動作はどうなるか。命運は全て \@ifnextchar の仕様に委ねられている。

幸いなことに、\@ifnextchar はとても良くできていて、「直後のトークンを検査する前に空白トークンを読み捨てる」という仕様になっている。なので、[bar] の前の空白トークンは捨てられて、「直後の文字」は [ だと判定される。このため結局以下のものが実行されることになる。

% [bar] の前の空白トークンが消えていることに注意
\xx@macroD@a{foo}[bar] {baz}

そして \xx@macroD@a を展開すると、期待通りに、

#1=foo, #2=bar, #3=baz

という引数が読み取られることが判る。(#3 は区切り無し引数なので、その前にある空白トークンは読み飛ばされる。)

つまり、\@ifnextchar を使ってオプション引数の括弧(変な括弧も含めて)を検査している限りは、「LaTeX の引数の規則」を逸脱することはないのである。

ということで

LaTeX の命令のインタフェース”としての TeX マクロを実装する際に、以下のような場合については、「引数の前に空白トークンがある場合」の動作を担保しておく必要がある。*4

  • “変な括弧”の必須引数をもつ場合。
  • オプション引数の存在の検査に \@ifnextchar を使わずに、自前で(\futurelet などを用いて)判定している場合。

そして、もっと重要な教訓として、「LaTeX パッケージを実装している TeX 者は、普通の LaTeX ユーザ以上に、LaTeX の仕様や習慣について敏感になる必要がある」ことを心に留めておこう。

*1:実際には先頭にオプション引数が 1 つあるが、ここではそれは考えないことにする。

*2:不正に空白トークンでないものを置いても通ってしまうが……。

*3:制御綴の直後に空白トークンは入ることは滅多にないので、必須引数の後にオプション引数のある形を用いた。

*4:自分の命令の仕様として「空白を許容しない」ということにするのは許容範囲内であるだろうが、その場合は、マニュアルにその旨を明記する必要があると、私は考えている。