マクロツイーター

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

本当は怖い if-トークンの話 (2)

前回の続きで、「if-トークンの罠」に陥らないため対策を述べる。実際に「罠」を構成するコードを修正する方法が中心となるが、危険なコードを避ける方法についても触れている。

問題を起こすコードを if 文の外に追い出す

つまり、「未定義かも知れない if-トークン」や「if 文でない if-トークン」が if 文の中にあることが問題なので、その部分をマクロにする等の方法で if 文の外に出してしまう。

% hoge の設定をコピーする
\def\xx@copy@left@hoge@to@right{%
  \let\ifxx@right@hoge\ifxx@left@hoge
}
% (当該の部分)
\ifxx@balanced@hoge
  \xx@copy@left@hoge@to@right
\fi

この処置はある程度「機械的に」行うことができるが、あまり闇雲に行うとコードの可読性を損なうことになるので、できるだけ、「意味のあるまとまりをマクロとして括り出す(そしてそのマクロに適切な名前を付ける、要するに利ファクタリングをするということ)」という形で行うのが望ましい。((今の場合、\xx@copy@left@hoge@to@right をその使用箇所から分離された「部品」と見做すということ。望ましくないのは、「追い出した部分」を if 外には置くが、別個の部品とせずに \ifxx@temp 等の名前を付けて元の if 文と一体と見做すことである。これでは単にコードが読み難くなってしまう。)) e-TeX 対応の例の場合は、例えば次のような対策が考えられる。

% if 文の外で \somecommand の実体を定義しておく
\@onlypreamable\xx@somecommand
\def\xx@somecommand#1{%
  \ifdefined#1%
     ………… % 何かの処理
  \fi
}
\ifxx@engine@is@eTeX %---- e-TeX の場合のみ実行する
  \let\somecommand\xx@somecommand
\fi                  %----
% 非 e-TeX の場合は \somecommand は未定義のままで、
% 無駄な \xx@somecommand は \@onlypreamble のために処分される。

私自身は、「if 文でない if-トークン」を含むコード((これには当然「if-トークンの定義」(\newif)も含まれる。特に、if 文中で \newif を実行するというのは、わざわざ自分で危険な「未定義かも知れない if-トークン」を作っていることになるので、絶対に避けるべきである。))は、それが実際に if 文中にあるかに関わらず、プログラム中に散在させることを避けて(パッケージ冒頭などの)特定の場所に纏めて置くことにしている。その理由は、後でパッケージの改修(例えばパッケージオプションを新設した、等)を行った際に、「コード中のある領域をまとめて if の中に入れる」ということが安全に行えるようにするためである。

%--------
%* ↓↓↓ このセクションは絶対に if 中に入れてはいけない!
\newif\ifxx@left@hoge        % 左をhogeにするか
\newif\ifxx@right@hoge       % 右をhogeにするか
\newif\ifxx@balanced@hoge    % hogeの設定を左右で均等にするか
% hoge の設定をコピーする
\def\xx@copy@left@hoge@to@right{%
  \let\ifxx@right@hoge\ifxx@left@hoge
}
%* ↑↑↑
%--------
%* このように危ないコードを一箇所に纏めておくと、
%* 残りの部分は安全に改修できるようになる。

これが最も基本的な対策だが、種々の理由で実施ができない(あるいは難しい)場合がある。以下では別の対策方法を述べるが、これら何れも当該部分のコードが少し技巧的に見えるという欠点がある。

\csname を利用する

「if 文でない if-トークン」の問題については、読み飛ばされる場合に見つけて欲しくない if-トークンや \fi\csname で隠してしまうという方法が使える。

\ifxx@balanced@hoge
  \expandafter\let\csname ifxx@right@hoge\expandafter\endcsname
   \csname ifxx@left@hoge\endcsname
\fi

しかし「未定義かも知れない if-トークン」をこれで解決しようとしても多くの場合失敗する。

% これは正しくない
\ifxx@engine@is@eTeX %---- e-TeX の場合のみ実行する
  \def\somecommand#1{%
    \csname ifdefined\endcsname#1%
       …………
    \csname fi\endcsname
  }
\fi                  %----

何故なら、もし \ifdefined が実際に実行されてその結果が偽だった場合、対応する \fi を「読み飛ばし」で見つける必要があるからである。上のコードでは対応する \fi が見つけられない。

ダミーの if や fi を置く

これも「if 文でない if-トークン」の場合に有効な対策の一つ。

\ifxx@balanced@hoge
  \let\ifxx@right@hoge\ifxx@left@hoge
  \@gobble\fi \@gobble\fi % if-トークンをマッチさせる
\fi

カテゴリコードに訴える

……使用を検討したことなら実際にある。

\ifxx@balanced@hoge
  \catcode`\|=0
  % 読み飛ばしの際には if-トークンに見えない
  |let|ifxx@right@hoge|ifxx@left@hoge
  \catcode`\|=12
\fi

別ファイルにして \input で読み込む

これは、「未定義かも知れない if-トークン」の使用が広大な領域に散在している場合に有効な対策である。典型的なのは、e-TeX 対応の例である。

% e-TeX 依存部分のコードを読み込む
\ifxx@engine@is@eTeX
  \input{mypkg-etex.def}
  %* mypkg-etex.def の中では、\ifdefined や \ifcsname も
  %* 含めて e-TeX 依存コードが使い放題になる
\fi

別ファイルにして \input で読み込む

先の例と同じ状況で、別ファイルを使わない解決策。e-TeX 依存コードをファイルの後ろの部分に纏めて記述して、非 e-TeX の場合はその領域に入る直前に \endinput で読込を途中で止めてしまう。

% パッケージ読込終了時の処理
\AtEndOfPackage{%
  %* ここに記述したコードはパッケージ読込終了時に実行される、
  %* つまり \ifxx@engine@is@eTeX の値に関わらず
  %* ファイルの一番最後にあるかのように扱われる。
}
% e-TeX でない場合はここで読込終了
\ifxx@engine@is@eTeX\else
  \expandafter\endinput
\fi
%------------
%% e-TeX 用コード
  %* ここでは e-TeX 依存コードが使い放題
%% EOF