2020年12月12日

\futurelet

この記事はTeX & LaTeX advent calendarの十二日目の記事です.昨日はZRさんでした.明日はtrueroadさんです.

人が少ないからというので登録したらその後一瞬で殆ど埋まっていました.ZRさんにだまされました.

以下のコードは全て\makeatletter内で実行されていると仮定します.

\futurelet

\futureletというTeXプリミティブがあります.使い方は次の通り.

\futurelet<token1><token2><token3>

\futureletが実行されると次のように変わります.

\let<token1>=<token3><token2><token3>

これだけみるとよくわかりませんが,このプリミティブはトークンの「先読み」に使われます.というか少なくとも自分はそれ以外の用途を知りません.

例を見てみましょう.とあるクラスファイルは,「別行見出しが連続している場合は,その続き具合に応じてスペースを変更する」という仕様を満たすために,別行見出しの直後にまた別行見出しの命令が来ているかを調べます.例えば,\section命令の直後に\section命令が来ているかと言うことを判定しています.このためには,\section命令の最後に次のトークンを「先読み」し,そのトークンが\sectionであるかを見ればよいです.で,その例ですが,\sectionの中身を全部書いたら大変なことになりますから,問題を簡略化して後続のトークンが\sectionならばAを,そうでなければBを出力する命令\checksectionを作ってみましょう.

\def\checksection{\futurelet\next@token\check@sectin@}% \futureletで「先読み」
\def\check@section@{%
  \ifx\next@token=\section % 続くトークンが\sectionならば
    A% 次が\sectionの時はAを出力
  \else
    B% 次が\sectionではない時はBを出力
  \fi
}

これを次のように使います.

\checksection\section{セクション}
\checksection\subsection{サブセクション}
\checksection $f\colon \mathbb{R}\to\mathbb{R}$を$C^{\infty}$関数とする.

最初だけAがでて,その後はBが出力されることが確認できます.さらに,そのほかの部分には一切影響を与えずに動いていることがわかります.

果たして何が起こったのでしょうか? 最初の定義に基づいてチェックしてみましょう.まず\checksection\section{セクション}というトークン列がどのように変化していくかを見てみます.

\checksection\section{セクション}
% \checksectionのマクロ展開
→ \futurelet\next@token\check@section@\section{セクション}
% \futureletの実行,<token1>=\next@token,<token2>=\check@section@,<token3>=\section
→ \let\next@token=\section\check@section@\section{セクション}
% \letが実行され,\next@tokenに\sectionが代入される.
→ \check@section@\section{セクション}
% \check@section@iの展開
→ \ifx\next@token=\section A\else B\fi\section{セクション}
% \next@tokenは\sectionなのでこの\ifxはtrueとなる.
→ A\section{セクション}

このように,途中で\next@tokenに後続のトークンが代入され,一方後続のトークンそのものも残っているため,見た目上後ろの方にはほぼ影響を与えずに後続トークンが何かによる場合分けを行うことができます.\checksection\subsection{サブセクション}の時は\next@token\subsectionが,\checksection $f\colon \mathbb{R}\to\mathbb{R}$を$C^{\infty}$関数とする.の場合は$が入り,これらの場合は\ifx\next@token=\sectionの判定が偽になります.

オプション処理

\futureletを使った処理の中で最もよく使われるのが,LaTeXコマンドのオプション引数の処理です.LaTeXにおいては,必須引数が{}で,省略可能なオプション引数が[]で囲まれるというルールがあります.これはTeXの仕様として存在するものではなく,\futureletによる先読みで実装されているものです.

最も単純な\testA[<オプション引数>]という仕様のマクロを定義してみましょう.LaTeXであれば,次のように\newcommandを使って定義できます.

\newcommand{\testA}[1][]{% オプション引数が省略された時は#1が空になる.

これを\futureletで実装してみましょう.\testAの次のトークンを先読みし,それが[であるかで場合分けをします.

\def\testA{\futurelet\next@token\testA@i}% 先読み開始.\testA@iにその後を託す
\def\testA@i{%
  \ifx\next@token[\expandafter\testA@ii\else\expandafter\testA@iii\fi
}
\def\testA@ii[#1]{% オプション引数があるときの処理
  [OPTION: #1]%
}
\def\testA@iii{% オプション引数がない時の処理
  [NO OPTION]%
}

実際にどう処理されていくか見てみましょう.まずオプション引数がある場合です.

\testA[A]
% \testAの展開
→ \futurelet\next@token\testA@i[A]
% \futureletの実行,<token1>=\next@token,<token2>=testA@i,<token3>=[
→ \let\next@token=[\testA@i[A]
% \next@tokenに[が代入される
→ \testA@i[A]
% \testA@iの展開
→ \ifx\next@token[\expandafter\testA@ii\else\expandafter\testA@iii\fi[A]
% \next@tokenの値は[なので,この\ifxは正と判定される.
→ \testA@ii[A]
% \testA@iiの展開
→ [OPTION: A]

次にオプションがない場合です.例えば\testA では……と続いているとしましょう.

\testA では
% \testAの展開
→ \futurelet\next@token\testA@i では
% \futureletの実行,<token1>=\next@token,<token2>=testA@i,<token3>=で
→ \let\next@token=[\testA@i では
% \next@tokenに「で」が代入される
→ \testA@i では
% \testA@iの展開
→ \ifx\next@token[\expandafter\testA@ii\else\expandafter\testA@iii\fi では
% \next@tokenの値は「で」なので,この\ifxは偽と判定される.
→ \testA@iii では
% \testA@iiiの展開
→ [NO OPTION]では

このように,\futureletを使うとオプション処理を行うことができます*1

それでもマクロでしたい?

Twitterをfuturelet lang:jaで検索するとこんなツイートが出てきました.したいこと次第ではありますが,\futureletを使わないでオプション処理をすることができることもあります.(おそらく無理なのではないかと思うケースもあります.実は先ほどの\testAのケースに関しては,無理なのではないかと思っています.)

そもそもマクロで先読みがしにくいのは何故でしょうか? 例えば,単に次のようなマクロを単純に考えてみましょう.\if系による面倒さを避けるために,\expandafter\@firstoftwo / \expandafter\@secondoftwoによる中身からの早期脱出を行っておきます*2

\def\testA#1{% 
  \ifx[#1\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi
    {\testA@ii[}%
    {\testA@iii#1}%
}
% \test@ii,\test@iiiはさっきと同じ.

展開の様子を見てみます.

\testA[A]
% \testAの展開,#1 = [
→ \ifx[[\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi{\test@ii[}{\test@iii}A]
% \ifx[[は正の判定
→ \@firstoftwo{\test@ii[}{\test@iii}A]
% \@firstoftwoは\def\@firstoftwo#1#2{#1}と定義されているマクロ.これが展開される.
→ \test@ii[A]
% \test@iiの展開.
→ [OPTION: A]

次に\testA ではの場合です

\testA では
% \testAの展開,#1 = [
→ \ifx[で\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi{\test@ii[}{\test@iii}A]
% \ifx[で は偽の判定
→ \@secondoftwo{\test@ii[}{\test@iiiで}は
% \@secondoftwoは\def\@secondoftwo#1#2{#2}と定義されているマクロ.これが展開される.
→ \test@iiiでは
% \test@iiの展開.
→ [NO OPTION]では

あれ,うまくいってそうですね.しかし,次の例で破綻します.\testA{\large で}ある実行してみると,「ある」が大文字のまま続いていることが確認できます.展開の様子をみてみましょう.

\testA{\large で}ある
% \testAの展開,#1 = \large で(!)
→ \ifx[\large で\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi{\test@ii[}{\test@iii\large で}ある
% \ifxで[と\large は異なるので偽の判定
→ \@secondoftwo{\test@ii[}{\test@iii\large で}ある
% \@secondoftwoの展開
→ \test@iii\large である
% \test@iiiの展開.
→ [NO OPTION]\large である

\ifxの判定も少し変なことになっていますが,それよりもグルーピングのつもりで使っていた{}が消えてしまっているのが問題です.「先読み」の条件として,基本的に後続のトークン列はそのまま保たれていて欲しいのですが,これがこの定義では実現できません.

マクロでオプション

それでもと頑張ってみましょう.以下は実用的に使ったことはないし,あまり検討もしていませんので,致命的欠陥があるかもしれませんのでご注意を.

では,グルーピングを取得しないですむようにしましょう.TeXでは{の直前までを引数として取得できる構文があります.

\def\cs#1#{% #1には{の直前までが入る

これを使って,{の直前まで取得し,一文字目が[か否かでわけることにします.次のように定義してみましょう.

\def\@endmark{\@endmark@}
\long\def\@gobbletoend#1\@endmark{}
\long\def\testA#1#{%
  \expandafter\@gobbletoend\ifx[#1\@empty\@endmark\expandafter\@firstoftwo\else\@endmark\expandafter\@secondoftwo\fi
    {\testA@ii#1}%
    {\testA@iii#1}
}

#1に入っているものが単一トークンでない場合にも動くようにしたのでちょっとややこしくなっていますが,気にしない.実際の展開をやってみましょう.次のようなソースを考えてみます.

\begin{document}
\testA[A]である.% ここを色々変える.
\begin{enumerate}
\item ....

まずはオプションをつけた場合です.

\testA[A]である.
% \testAの展開.#1 = [A]である\begin
→ \expandafter\@gobbletoend\ifx[[A]である\begin\@empty\@endmark\expandafter\@firstoftwo\else\@endmark\expandafter\@secondoftwo\fi
    {\testA@ii[A]である\begin}%
    {\testA@iii[A]である\begin}
% \expandafterにより\@gobbletoendの展開は抑制され,\ifxが展開される.判定は正.
→ \@gobbletoend A]である\begin\@empty\@endmark\expandafter\@firstoftwo\else\@endmark\expandafter\@secondoftwo\fi
    {\testA@ii[A]である\begin}%
    {\testA@iii[A]である\begin}
% \@gobbletoendの展開,#1=A]である\begin\@empty
→ \expandafter\@firstoftwo\else\@endmark\expandafter\@secondoftwo\fi
    {\testA@ii[A]である\begin}%
    {\testA@iii[A]である\begin}
% \else...\fiの展開
→ \@firstoftwo
    {\testA@ii[A]である\begin}%
    {\testA@iii[A]である\begin}
% \@firstoftwoの展開
→ \testA@ii[A]である\begin
% \test@iiの展開
→ [OPTION: A]である\begin

うまく行きます.さっき失敗した例ではどうでしょうか? \testA{\large である}としてみましょう.

\testA{\large である}
% \testAの展開,#1は空っぽ
→ \expandafter\@gobbletoend\ifx[\@empty\@endmark\expandafter\@firstoftwo\else\@endmark\expandafter\@secondoftwo\fi
    {\testA@ii}%
    {\testA@iii}
% \expandafterにより\@gobbletoendの展開は抑制され,\ifxが展開される.[と\@emptyは異なるので判定は偽.
→ \@gobbletoend\@endmark\expandafter\@secondoftwo\fi
    {\testA@ii}%
    {\testA@iii}
% \@gobbletoendの展開
→ \expandafter\@secondoftwo\fi
    {\testA@ii}%
    {\testA@iii}
% \fiの展開
→ \@secondoftwo
    {\testA@ii}%
    {\testA@iii}
% \@secondoftwoの展開
→ \testA@iii
% \test@iiiの展開
→ [NO OPTION: A]

やはりうまく言っているようです.今度は{\large である}の部分は引数としてとらないので,先ほど述べたようなことも起こりません.

しかし世の中うまくはいきません.\testAが引数としてとる#1はかなり長くなってしまうことが起こります.もちろん最後に戻しているので一件問題なさそうですが,カテゴリーコードの変更に対処できません.そう,いっつも迷惑かけてくるあの\verbが立ちはだかります.

\testA \verb|a^2| % ! Missing $ inserted.

それでもマクロでどうにかしたいんだけど.

以上の方法は比較的「近い」場所にグルーピング開始の文字があれば使えそうです.もし,\testAがその後に必須引数を持っていれば,つまり\testA[<オプション引数>]{<必須引数>}という仕様の命令ならば,必須引数のためのグルーピングがあるのでその前まででとまります.というわけで,次のようにしてみましょう.

\def\@endmark{\@endmarkin}
\long\def\@gobbletoend#1\@endmark{}
\long\def\testA#1#{%
  \expandafter\@gobbletoend\ifx[#1\@empty\@endmark\expandafter\@firstoftwo\else\@endmark\expandafter\@secondoftwo\fi
    {\testA@ii#1}%
    {\testA@iii#1}
}
\def\testA@ii[#1]#2{% オプション引数があるときの処理
  [OPTION: #1][ARG: #2]%
}
\def\testA@iii#1{% オプション引数がない時の処理
  [NO OPTION][ARG: #1]%
}

オプション処理部分はさっきと同じです.ただし,今回は必須引数があるという仕様なので,\testA@ii\testA@iiiにはその必須引数の処理を入れてあります.またこれにより,正しい仕様に基づいた入力が行われていれば,\testA#1に入ってくるのはオプションそのものか,空っぽになるかのはずです.従って,さっきのような全然関係ない部分の\verbが混じり込むこともありません.

ちなみにxparseパッケージにはには\NewExpandableDocumentCommandという命令があり,同パッケージの提供する\NewDocumentCommandと同じ書式で展開可能な命令を作ることができます.内部動作は(使ってみたところから予測するに)\def\testA#1{...として,トークン列#1そのものが全体として[のみであるかという判定をしているようです.従って,\testA{[}のようにするとエラーになります.(これはxparseのドキュメントにも書いてある.)上のような実装には何か問題があるのかもしれません.

*1
\newcommandで定義されたコマンドはもう少し色々な処理を行うので,今定義したものとは挙動が異なります.
*2
二日目のbd_gfngfnさんの記事に現れた\hopと似たような形です.

0 件のコメント:

コメントを投稿

コメントの追加にはサードパーティーCookieの許可が必要です