マクロツイーター

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

今さら人に聞けない zref パッケージのキホン(1)

↑誰も聞いてないって……。

……というツッコミはさて措き。前回の記事のネタで登場した zref パッケージは「極めて難解でかつ便利なパッケージ」だと思うので、それについて解説する。

幸せな LaTeX 者の皆さんへ

以下の節では zref パッケージの「開発者向けの機能」について解説していて、これらは一般の LaTeX ユーザには無縁のものである。しかし、zref パッケージにはユーザ(LaTeX 文書作成者)向けの機能も多く存在し、その中には非常に面白いもの(“コンパイルされた回数を出力する”、“次のページの番号を出力する”など)が存在する。なのでこれらの機能について把握しておくのは有用であると思う。

zref の“ユーザ向け機能”はマニュアルの 3 節で説明されている。しかし残念ながら、“開発者向けに機能”の説明と混ざっていて少し読みにくくなっている。マニュアルを読む際には次のことを覚えておくといいであろう。

命令の名前に @ を含むもの(\zref@iflastpage など)は“開発者向け機能”である(だから無視してよい)。名前に @ を含まないもの(\ziflastpage など)は“ユーザ向け機能”である。

(「幸せな LaTeX 者向け文章」はここでオシマイ。)

zref とは何か

zref パッケージは「LaTeX における相互参照の仕組みを一般化するパッケージ」である。少し言い方を変えると、「2 パス処理の実装を支援するパッケージ」とも言える。

zref が提供する「一般化した相互参照のモデル」を、LaTeX の数式の相互参照と対照する形で説明する。

  • 値の読み書きができる「変数」を用意する。例えば数式の相互参照においては、『式番号』『(数式が配置される)ページ番号』などの「変数」を用意することになる。
  • 文書の任意の箇所に「ラベル」を配置することで、その「ラベル」に対して、その箇所における「変数」の値が紐付けられた上で、その状態が補助ファイル(.aux)に記録される。例えば、数式を書いた直後に \label{eq:abc} を実行すると、「ラベル“eq:abc”の位置では、『式番号』は“5.1”で、『ページ番号』は“42”であった」という情報が .aux ファイルに記録される。
  • コンパイルを反復したときに、補助ファイルに記録された「前回コンパイル時の、ラベルと変数値の紐付け」の情報を読み出すことができる。例えば、前項の状況を仮定すると、次回のコンパイルでは \ref{eq:abc} で「5.1」、\pageref{eq:abc}「42」と出力される。

zref パッケージでは、このような仕組を「一般的に使える形で」提供する。LaTeX の相互参照の実装とは全く独立に実装されて(従って別の命令群を用いる)いるので、既存の動作に影響を与えることはない。

zref で「最後のコマンド」してみる

これを実装してみよう。

すなわち次の仕様を満たす命令 \NewLastDoCommand を実装する。

  • \NewLastDoCommand\命令{<非最後>}{<最後>} : 以下の動作を行う命令を定義する。文書中の最後の出現については <最後> を実行し、それ以外は <非最後> を実行する。(完全展開可能ではない。)
\NewLastDoCommand\hoge{Hoge}{LastHoge}
\hoge, \hoge, \hoge!
%==>「Hoge, Hoge, LastHoge!」と出力

\hoge が何回呼び出されたかは 1 回文書を最後まで処理しないと判らない。そのため(実用可能なレベルでは)2 パス処理が必須になる事例といえる。

実装方針
  • \NewLastDoCommand で定義された命令(これを「lastdo 命令」と呼ぶことにする;例えば \hoge)の各々について、lastdo@hoge という「変数」を用意する。
  • \hoge が呼び出される度に、「変数」lastdo@hoge の値を 1 増やす。
  • 文書の最後に“lastdo:”という「ラベル」を置く。これで、文書中で各 lastdo 命令が呼び出された回数が .aux に記録される。
  • 前回コンパイルの“lastdo:”ラベルに紐付けられた「変数」lastdo@hoge の値が「\hoge が呼び出された回数」になるので、この情報を利用して、\hoge において「非最後」と「最後」のどちらを実行するかを決定する。

「文書の最後にラベルを置く」というのがポイントである。

それでは、早速実装を始めよう。zref を利用するので、ますはこれを読み込むことになる。

\RequirePackage{zref}
実装開始

先の方針に基づくと、\NewLastDoComamnd に渡された lastdo 命令の制御綴 \hoge について、“hoge”という文字列が必要になってくる。もちろん、これを得るのは容易で「\expandafter\@gobble\string\hoge」でいいのだが、これを zref パッケージの命令の引数に直接使って受け付けられる保証はない*1ので、展開済の文字列が引数として欲しい。従って、以下のようにして「マクロの乗り継ぎ」をする。

%<+> \NewLastDoCommand\命令{<非最後>}{<最後>}
\newcommand{\NewLastDoCommand}[3]{%
  \@ifdefinable{#1}{% \命令 が定義可能なら
    % 命令名を抽出して \lastdo@new@a に進む
    \edef\next{\noexpand\lastdo@new@a
      {\expandafter\@gobble\string#1}}%
    \next#1{#2}{#3}}}
%% \lastdo@new@a{<命令名>}\命令{<非最後>}{<最後>}
\def\lastdo@new@a#1#2#3#4{%
  ……
}

つまり次のように実行される。

\\NewLastDoCommand\hoge{Hoge}{LastHoge}
-->\lastdo@new@a{hoge}\hoge{Hoge}{LastHoge}

\lastdo@new@a の中でやるべきことは 2 つある。

  1. 「変数」lastdo@hoge を作成する。
  2. マクロ \hoge を適切に定義する。

2 については、一旦マクロ \lastdo@do に委譲してそれの実装を後で考えることにしよう。

%(\lastdo@new@a の定義本体の中)
  \gdef#2{\lastdo@do{#1}{#3}{#4}}
  % 例えば \gdef\hoge{\lastdo@do{hoge}{Hoge}{LastHoge}}%
プロパティを作る

残りは 1 の「変数を作る」処理であり、いよいよ zref を使った処理になる。これまで「変数」と言っていたものは zref では「プロパティ(property)」と呼ばれる。プロパティには任意のトークン列が読み書きできる。新しいプロパティを作るには \zref@newprop 命令を利用する。

%(\lastdo@new@a の定義本体の中; #1は"hoge"などのlastdo命令名)
  % プロパティ "lastdo@hoge" を作成する
  \zref@newprop{lastdo@#1}[0]{0}%

第 3 引数は(今回の実行での)初期値を指定する。回数を数えたいので、ここでは「0」とする。第 2 引数(オプション)には“相互参照が未定義”である場合に使われる値を指定する。省略した場合は「\reset@font\bfseries ??」というトークン列*2が使われるが、今の用途だと「値」が整数(を表すトークン列)でないと困るのでここでも「0」を設定する。(つまり、初回の実行時(.aux が存在しない場合)は全ての lastdo 命令の実行回数が「0 回」と見なされるわけである。)

これで完成のようにも思えるが実はそうではない。zref では、プロパティを「リスト(list)」と呼ばれるグループに分類して管理していて、プロパティは必ずどれかのリストに所属させる必要があるのである。(その理由は後述。)ここでは、LastDo というリストを新たに作って lastdo@... のプロパティは全て LastDo に登録することにする。

%(トップレベル)
\zref@newlist{LastDo}
%(\lastdo@new@a の定義本体の中)
  \zref@addprop{LastDo}{lastdo@#1}%

これで \lastdo@new@a が完成した。

%% \lastdo@new@a{<命令名>}\命令{<非最後>}{<最後>}
\def\lastdo@new@a#1#2#3#4{%
  % プロパティ 'lastdo@命令名' を作成する
  \zref@newprop{lastdo@#1}[0]{0}%
  \zref@addprop{LastDo}{lastdo@#1}%
  % 命令を定義する
  \gdef#2{\lastdo@do{#1}{#3}{#4}}}
(続く)

*1:まあ、Oberdiek さんのことだから、そのあたりの配慮はできていると思うが…。

*2:周知の通り、LaTeX で“相互参照が未定義”の場合には値の代わりに太字で「??」と出力される。この挙動と合わせているわけである。