マクロツイーター

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

LuaTeXで“relax化しないcsname”をもっとつくる件

前回の記事で「\relaxせずに\csnameする」クイズについて、Luaコードを利用して実装する正解例を示した。

ところで、例のクイズの補足説明を改めて見ると、少し気になる内容がある。

※もちろんLuaコードを使ってよい。
※ただしLuaコードを使わない回答ができればもっとよい🙃

実はこの問題はLuaコードを使わない(ただしLuaTeXの機能は利用する)解決法が存在する。本記事ではその方法を解説してみる。

必要な前提知識

  • フツーのTeX言語🤮の知識。

TeXの常識、LuaTeXの非常識

前回の問題の考察において、「“relax”化が起こった場合に代入操作で再度未定義の状態に戻す」という案を考えた。

\csnameによる“relax化”が避けられないなら、その後でもう一度その制御綴に未定義を代入すればよい。しかし今回の問題では完全展開可能で実装する必要があるため、代入操作は使えない。

完全展開可能の実装で代入は使えないというのは、TeX言語の常識である。……といいたいところだが、実はLuaTeXでは違う😲😲😲 LuaTeX拡張には「展開操作で代入を行うためのプリミティブ」が用意されている1のである。

  • \immediateassignment‹代入文›: [展開可能] 空トークン列に展開されるが、その副作用として‹代入文›で指示された代入が行われる。

ただしここで指示できる代入文には制限があるようである。例えばボックス代入は許されていない。

代入が可能であるのなら、以下のような素直な手順で要件が実現できるはずである。

  • まず\ifcsnameで当該制御綴が定義済かを判定する。
  • 判定が済んだ2ので普通に\csnameで制御綴を生成する。判定結果が真(定義済)だった場合はここで終了。
  • 判定結果が偽(未定義)だった場合、既に制御綴が手元にあるので、\immediateassignment付きの代入で制御綴を未定義に戻してから制御綴を置く。

とりあえず書いてみる

先の方針を素直にコードで表すと以下のようになる。

\def\myMakeCS#1{%
  % まず判定する
  \ifcsname#1\endcsname
    % 定義済の場合は簡単
    \csname#1\endcsname
  \else
    % 未定義の場合.
    % ここで`\csname`を1回展開して制御綴を作る(relax化発生)
    \expandafter\my@make@cs@undef\csname#1\endcsname
  \fi
}
\def\my@make@cs@undef#1{% #1は目的の制御綴
  % 制御綴を未定義に戻す
  \immediateassignment\let#1\my@undefined
  % 展開結果
  #1%
}

全体の動作だけみると、この実装で既に想定通りになっている。

% 完全展開可能性の確認のため \message 中で実行.
% \futurelet は展開不能なのでこれが最終結果になる.
\message{\myMakeCS{futurelet}}
%==>「\futurelet」と表示
% 未定義の制御綴の場合, (relax化せず)未定義エラーが出る.
*\message{\myMakeCS{duckduck}}
%==> \duckduck の箇所でエラー「! Undefined control sequence.」

しかしこの実装は要件を満たしていない。まず問題なのは「生成された制御綴よりも後方に\fiが残るせいで(完全展開可能ではあるが)先頭完全展開可能になっていない」ことである。仮に“後方の部分”を無視したとしても、「if文が真の場合と偽の場合で制御綴に到達するまでの展開回数が異なる」という問題が残っている。

これらの問題を解決するために「展開の加速」という技法を利用することにする。展開の加速については以前に別の記事で紹介した。

ここでは\expandedを利用して完全展開可能な(しかし先頭完全展開可能とは限らない)マクロを「2回展開」に加速する手法3を用いる。これによって(マクロが常に2回展開になるので)先述の2つの問題が同時に解決できてしまう。

では早速マクロを修正してみる。\expandedによる加速を適用するのは簡単で、マクロの定義本体を\expanded{…}で囲うだけでよい。

\def\myMakeCS#1{%
  \expanded{% 加速する
    \ifcsname#1\endcsname
      \csname#1\endcsname
    \else
      \expandafter\my@make@cs@undef\csname#1\endcsname
    \fi
  }%
}

ただ今の要件には少し厄介な点があり、それは「制御綴が現れたらそこで展開を止める必要がある」ことである。これは\unexpandedプリミティブ4を使えば解決できる。例えば\my@make@cs@undefマクロの最後の#1(=所望の制御綴)はそれ以上展開してほしくないので\unexpandedで囲えばよい。

\def\my@make@cs@undef#1{% #1は目的の制御綴
  \immediateassignment\let#1\my@undefined
  % これが展開結果なので \unexpanded で展開を止める
  \unexpanded{#1}%
}

\myMakeCS中の\csname#1\endcsnameは、それを1回展開してそこで止まってほしい。これは\unexpanded\expandafterと組み合わせる5ことで解決できる。

      \unexpanded\expandafter{\csname#1\endcsname}%

これで要件通り(n=2)に動作するプログラムが完成したことになる🙂

正解例(Luaしないやつ)

最終的なプログラムは以下のようになった。

%% \myMakeCS{‹文字トークン列›}: 2回展開すると`‹文字トークン列›`を
% 名前とする制御綴になる.
\def\myMakeCS#1{%
  \expanded{%
    \ifcsname#1\endcsname
      \unexpanded\expandafter{\csname#1\endcsname}%
    \else
      \expandafter\my@make@cs@undef\csname#1\endcsname
    \fi
  }%
}
\def\my@make@cs@undef#1{% #1は制御綴
  % 制御綴を未定義に戻す
  \immediateassignment\let#1\my@undefined
  \unexpanded{#1}%
}

前回と同じ例を試してみる。こちらも期待通りに動いているようだ😌

\let\XA\expandafter
\XA\XA\XA\show\myMakeCS{noexpand} % 定義済の制御綴
%==>「\noexpand=\noexpand.」と表示
\XA\XA\XA\show\myMakeCS{duck?duck!} % 未定義の制御綴
%==>「\duck?duck!=undefined.」と表示
\XA\XA\XA\show\myMakeCS{"アレ\string\?} %引数は「"アレ\?」
%==>「\"アレ\?=undefined.」と表示

おまけ(Luaしない別のやつ)

この投稿をしたときに用意していたプログラムは以下のものだった。

\def\myMakeCS#1{%
  \romannumeral-`>\ifcsname#1\endcsname
    \expandafter\my@make@cs@a\expandafter\space
  \else \expandafter\my@make@cs@a\expandafter\my@make@cs@b
  \fi{#1}}
\def\my@make@cs@a#1#2{%
  \expandafter#1\csname#2\endcsname}
\def\my@make@cs@b#1{%
  \immediateassignment\let#1\@undefined
  \space#1}

このプログラムでは\expandedの代わりに\romannumeralトリックによる先頭完全展開を利用している6

まとめ

というわけで、キャンペーン🌸🍀期間内でも期間外でもとにかく皆さん、ドンドンTeX言語🤮しましょう!💁


  1. なお「LuaコードでTeXパラメタへの代入を行う」ことでも実質的に「展開操作で代入」を行える。しかし今回の問題については「制御綴に“未定義”を代入する」をLuaコード上で行う方法が自分には発見できなかった😢
  2. 判定が済む前に\csnameを使ってしまうと元々未定義だったのか\relaxだったのかが区別できなくなり失敗する。
  3. 参考記事にあるように「Luaしない」前提であれば加速は「2回展開」までが限界である。前回の正解例は実質的にLuaで実装されているので、参考記事の技法を使えば「1回展開」に加速することが可能である。
  4. \expanded{…}で完全展開しているトークン列の中に\unexpanded{‹何か›}があった場合、その展開結果は‹何か›になりそれ以上展開されない
  5. \unexpandedはグループを引数にとるプリミティブなので、中を1回展開したい場合に\unexpandedの前に\expandafterを付ける必要はない。
  6. \romannumeralトリックによる先頭完全展開を途中で止めたい場合は、トークン列の先頭にわざと空白トークンを生じさせればよい。