マクロツイーター

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

\noexpandを完全攻略しない理由

今年も例によって始まりました🌸

というわけで、早速\noexpandについて徹底解説する記事を書きました。

記事中でも述べられていますが、この記事の大きな特徴は「\noexpand定義」特に「\noexpand‹トークン›を一回展開した結果がどうなるかの(一般的に通用する)規則」を示すことを敢えて避けているということです。

ソレの地獄絵図

\noexpandの定義」を追求するのは危険です。次のような例をみてみましょう。

[test.tex]
% plain TeX
\def\xA{\noexpand\xB}
\def\xB{\noexpand\xC}
\def\xC{\noexpand\xD}
\def\xD{\noexpand\xE}
\def\xE{\noexpand\xF}
\def\xF{pt}
\dimen0=1 \xA\relax
\message{RESULT=\the\dimen0}
\bye

このplain TeXコードには\noexpandが大量にあります。もし仮にこれらの\noexpandないとすると、\xAは結局ptに展開されるため\dimen0レジスタに1ptが代入されて、その結果RESULT=1.0ptが端末に表示されます。それでは元々の\noexpandがあるコードを実行するとどうなるでしょう。

test.texをetexコマンド(pdfTeXエンジン)で実行すると実際にRESULT=1.0ptが表示されました。

This is pdfTeX, Version 3.141592653-2.6-1.40.29 (TeX Live 2026) (preloaded format=etex)
 restricted \write18 enabled.
entering extended mode
(./test.tex RESULT=1.0pt )
No pages of output.
Transcript written on test.log.

ところが、同じtest.texをtexコマンド(元祖TeXエンジン)で実行すると「! Illegal unit of measure」のエラーが発生します。

This is TeX, Version 3.141592653 (TeX Live 2026) (preloaded format=tex)
(./test.tex
! Illegal unit of measure (pt inserted).
<to be read again>
                   p
<to be read again>
                   t
l.8 \dimen0=1 \xA
                 \relax
?

確かにpdfTeXでしか使えない単位(pxndなど)は存在しますが、ここで使っているのはptなので元祖TeXでだけエラーになる理由を説明するのは困難1でしょう。

この例の特に厄介なところは、このコードに「一見重要そうでない変更」を加えただけでエラー発生の有無が変わることです。

  • 元のtest.texでは\xAからptへの展開を“6段”(\xAから\xFまで)にしていますが、これを“5段”(\xEまで)に減らすとetexとtexの両方で通るようになります。
  • 逆に“7段”(\xGまで)に増やすとetexとtexの両方でエラーが発生します。
  • 元の“6段”に戻した上で\xFの定義をptからmmに変えると、再びetexとtexの両方で通る(RESULT=2.84526ptと表示)ようになります。

これを前提にした上でもし「一般的な\noexpandの定義」を用意しようとするなら、当然その定義はこの極めて不可解な結果を説明できるものにする必要があります。それが絶望的に困難であることは容易に想像できます。仮に、そういう定義ができたとしてもそれは極めて複雑な規則になるでしょうから、そもそも一般原則を習得することのメリットがかなり乏しくなります。\noexpandが実際に使われるパターンが数少ないことも加味すると、一般原則の追求は賢明ではないのです。

\relax する話の真相

TeX言語に詳しい人なら「\noexpand‹トークン›」の展開結果について、「意味が “一時的に”\relax(あるいは“\relaxモドキ”2)に置き換わった状態の‹トークン›になる」という説明を聞いたことがあるかもしれません。この説明自体は間違ってなくて、実際に展開結果を\showで調べた結果とも整合します。

% "\noexpand\jobname"を1回展開して得られるトークンの意味を調べる.
\expandafter\show\noexpand\jobname
%==> "\jobname=\relax"と表示される

この定義の問題点は「“一時的”が具体的にいつまでか判らない」ということです。意味が\relaxに置き換わる効果がいつまで持続するのかが判っていないと、肝心の「具体的なTeXコードの動作を導出する」という目的には役に立ちません。

実際に“一時的”の範囲を明確にしようとすると、TeX処理系の個々の内部動作の手順を把握する必要があります。先ほどの“地獄絵図”において元祖TeXとpdfTeXで動作が異なる原因は「サポートする単位の種類がpdfTeXの方が多いため、単位をスキャンする際の内部動作が両者で相違する」からです。「単位をスキャンする際の内部動作を把握する必要がある」なんて事態はほぼ誰も望まないところでしょう。

ソレの豆知識

このように「一般的な状況での\noexpand‹トークン›の展開結果」の把握は困難なわけですが、実は‹トークン›展開不能である場合に限っては極めて単純明快な規則があります。

  • ‹トークン›展開不能であるとき、\noexpand‹トークン›の一回展開の結果は‹トークン›になる。

これは知っておく価値が十分にあると思います。元のQiitaの記事で扱った「トークンの展開可能性を判定する条件文」もこの規則が根拠になっています。

% ‹トークン›が展開不能ならば
\expandafter\ifx\noexpand‹トークン›‹トークン›

この条件文は「\noexpand‹トークン›の1回展開」と‹トークン›\ifxで比較していますが、ここで‹トークン›が展開不能であれば前者は単なる‹トークン›なので当然後者と\ifx-等価になる3わけです。

また、当該の規則を知っておくと、次のような変則的な事例についても結果を予測できます。

% plain TeX
\noexpand\$
\bye
% ※plainでの"\$"の定義はchardefトークンの「\char"24」

これの実行結果はどうなるでしょうか。chardefトークンは展開不能であるため\noexpand\$の展開すると\$になるため普通に「$」が印字されます。

まとめ

というわけで、今年も皆さん💁


  1. さらに奇妙なのは、エラーコンテキスト表示から判るように\xAptまで展開されているということです。つまり、スキャンしている箇所に実際にはptがあるのに「単位がない」というエラーになっています。
  2. 実行時の動作\relaxプリミティブと全く同じだが\relaxプリミティブと\ifx-等価にならない特殊なトークンのことを指します。
  3. 逆に‹トークン›が展開可能の場合は、\noexpand‹トークン›の1回展開は「意味が“\relaxモドキ”であるトークン」になります。展開可能である以上‹トークン›の意味が“\relaxモドキ”では決してありえないので、判定は確実に偽になります。