マクロツイーター

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

メモ:新しいLaTeXの文書プロパティ機能の現状

コレに関する話。

blog.wtsnjp.com

文書プロパティについて語りたい

この記事の説明(オリジナルのLaTeX Newsの内容もほぼ同じ)を読むと、文書プロパティ1は従来の相互参照(\label\ref)の機構を拡張するもので、しかも値を“展開可能”な方法で取得可能であると説明されている。これを見る限り、この新機能によって、長年TeX言語プログラマを悩ませてきた「ラベルに紐づく値を取得する確実な方法がない」という問題が解決されるように思える。

しかし実際にチョット調べてみたところ、少なくとも現状ではこの希望的観測は的外れで、実際には文書プロパティの機能は「ラベルに紐づく値の取得」には使えなかった(ざんねん🙃)、という話。

「ラベルに紐づく値の取得」が難しい

以下にLaTeXのフツーの相互参照を利用した文書を示す2

\documentclass[a4paper]{article}
\begin{document}
\setcounter{section}{41}% 節番号を42から始める
\section{Duck}\label{sec:duck}% ラベルを付けた
Quack!
\section{Conclusion}
Section~\ref{sec:duck} (p.~\pageref{sec:duck}) is dull.
\end{document}

出力

この文書ソース中で、\ref{sec:duck}\label{sec:duck}を付与した節の番号、\pageref{sec:duck}は当該の節のあるページの番号を出力するために使われている。ここで注意すべきなのは、LaTeXの仕様としては\ref\pagerefはあくまで番号を「出力」する命令であり、番号を「取得」するための手段は用意されていない、ということである。

相互参照の使い方によっては単に番号を出力する以外の使い方をしたい場合もある。ここでは(極めて人工的な例であるが)次のような命令を実装することを考えてみよう。

  • \myRefSum{‹ラベル›}‹ラベル›に紐づくカウンタ3の番号(\ref{‹ラベル›}の値)とページ番号(\pageref{‹ラベル›}の値)の合計の値を出力する。

この命令を実装しようとすると、\ref\pagerefを単純に使って番号を出力するだけでは間に合わず、\ref\pagerefの値を(トークン列なり内部整数値なりの形で)取得する必要がある。しかしLaTeXの仕様ではそもそも値を取得するための手段が用意されていないので、結局、仕様に従う限りは実装は不可能になってしまう。

どうしても\myRefSumを実装したいのであれば「LaTeXカーネル内部実装に依存するコードを書く」という強硬策に頼る4ことになるが、その場合でも現実問題として相互参照周りの内部実装は様々な要因5で変動しやすいため、「特定の仕様通りに確実に動作する」ようなコードを実装する(そして維持する)のは極めて困難なのである。

新機能で「ラベルに紐づく値の取得」ができたらよいのに

新しい文書プロパティ機能では\RefPropertyという展開可能な命令でプロパティの値を取得できる。

  • \RefProperty{‹ラベル›}{‹プロパティ›}:[展開可能]‹ラベル›に紐づくプロパティ‹プロパティ›の値。

プロパティを定義及び記録する命令6は従来の\label\refとは別に存在するのであるが、一方で仕様書(ltproperties-doc.pdf)には次のようなことが書かれている。

  • カーネルで予めlabelpageというプロパティが定義されている。
  • labelは従来の相互参照における\refの値(ラベルに紐づくカウンタ番号出力)に相当する。
  • pageは従来の相互参照における\pagerefの値(ラベルに紐づくページ番号出力)に相当する。
  • 従来の\labelの「ラベル」はそのまま文書プロパティ機能における「ラベル」にもなる。

これを読む限りは、いかにも次のような仕組みになっていそうである。

  • \RefProperty{‹ラベル›}{label}により\ref{‹ラベル›}の値が取得できる。
  • \RefProperty{‹ラベル›}{page}により\pageref{‹ラベル›}の値が取得できる。

本当にそうなっているのか確かめてみよう。

\documentclass[a4paper]{article}
\begin{document}
\setcounter{section}{41}
\section{Duck}\label{sec:duck}
Quack!
\section{Conclusion}
label=\RefProperty{sec:duck}{label};
page=\RefProperty{sec:duck}{page}.
\end{document}

出力

ありゃ、うまくいかない(ざんねん🙃)

値の部分にはプロパティの「既定値」が出力されている。どうやら、ラベルに紐づくプロパティの値が記録されていないようである。つまり、少なくとも現状の仕様においては、従来の\label命令では新機能のプロパティの値は記録されないようにみえる。

余談:値が取得できないなら警告すべきでは

先の文書のビルドの際には警告は出ないのであるが、取得するプロパティ値が記録されていないなら(従来の相互参照において指定した\labelが見つからない時と同様に)警告が出てほしい気がする。実は警告を出す命令は別にある。

  • \RefUndefinedWarn{‹ラベル›}{‹プロパティ›}‹ラベル›に紐づくプロパティ‹プロパティ›の値が記録されていなければ警告を出す。

なぜ取得する命令\RefPropertyで警告を出さないのかというと、そうすると展開可能でなくなってしまうからである。\RefPropertyを使うプログラマが“適切なタイミング”で適宜\RefUndefinedWarnを実行する必要がある。

先の文書ソースの7行目に次のコードを追記する。

\RefUndefinedWarn{sec:duck}{label}
\RefUndefinedWarn{sec:duck}{page}

すると文書のビルド時に警告が出るようになる。

LaTeX Warning: Property `label' undefined for reference `sec:duck' on page 1 on
 input line 7.

LaTeX Warning: Property `page' undefined for reference `sec:duck' on page 1 on
input line 8.

やはりプロパティ値は記録されていないことが判明した。

どうにかして「ラベルに紐づく値の取得」してみる

当然であるが、「\labelを実行する際に新機能のプロパティの値を同時に記録するようにする」と\RefPropertyで値が取得できるようになる。例えば、以下のような命令\myLabelを定義してこれを\labelの代わりに使うという方法が考えられる。

※先述の通り相互参照と文書プロパティの「ラベル」は共通(名前空間を共有する)なので、同じ名前のラベルを両方に使うことはできない(ラベル重複になってしまう)。そのため、プロパティの方のラベルの名前には接頭辞(my/)を付けている。

%% \myLabel{<ラベル>}: 相互参照と文書プロパティの両方のラベルを置く.
% ※プロパティの方のラベルには"my/"の接頭辞を付ける.
\NewDocumentCommand\myLabel{m}{%
  \label{#1}% 相互参照のラベル配置
  \RecordProperties{my/#1}{label,page}% プロパティ記録
}

これにより\RefPropertyで値が実際に取得できるようになるので、今度は先述の\myRefSumの実装が可能になる。実際に\RefPropertyでの値の取得と\myRefSumの実行をする完全なコードを以下に示した。

\intevalは整数式の計算をする命令。

\documentclass[a4paper]{article}
%% \myLabel{<ラベル>}: 相互参照と文書プロパティの両方のラベルを置く.
% ※プロパティの方のラベルには"my/"の接頭辞を付ける.
\NewDocumentCommand\myLabel{m}{%
  \label{#1}% 相互参照のラベル配置
  \RecordProperties{my/#1}{label,page}% プロパティ記録
}
%% \myRefSum{<ラベル>}: 例のアレ.
\NewDocumentCommand\myRefSum{m}{%
  % 値が記録されてなければ警告
  \RefUndefinedWarn{my/#1}{label}%
  \RefUndefinedWarn{my/#1}{page}%
  % 両方の値が記録されていれば合計値を出力する
  \IfPropertyRecordedTF{my/#1}{label}{%
    \IfPropertyRecordedTF{my/#1}{page}{%
      % 展開可能なので \inteval 中で使用可能
      \inteval{\RefProperty{my/#1}{label}%
               +\RefProperty{my/#1}{page}}%
    }{}%else
  }{}%else
}
\begin{document}
\setcounter{section}{41}
\section{Duck}\myLabel{sec:duck}
Quack!
\section{Conclusion}
% 番号を取得する例
label=\RefProperty{my/sec:duck}{label};
page=\RefProperty{my/sec:duck}{page}.\par
% \myRefSum の例
sum=\myRefSum{sec:duck}.
\end{document}

出力

なぜこんな仕様なのか

仕様書(ltproperties-doc.pdf)に次のような記述がある。

Currently the code has nearly no impact on the main \label and \ref commands as too many external packages rely on the concrete implementation. There is one exception: the label names share the same namespace. That means that if both \label{ABC} and \RecordProperties{ABC}{page} are used there is a warning Label ‘ABC’ multiply defined.

(訳)
現状では、大本の\labelおよび\ref命令はこの[新機能実装の]コードの影響をほぼ受けない。外部パッケージでその具体的な実装に依存したものがあまりに多いからである。一つだけ例外がある:ラベルの名前は名前空間を共有している。つまり、\label{ABC}\RecordProperties{ABC}{page}の両方を使うと、Label ‘ABC’ multiply definedの警告が発生する。

従来の相互参照と文書プロパティを「全く無関係な別個のもの」と位置付けるならば、両者でラベルの名前空間を共有するのは明らかに不合理なはずである。共有させているということは、恐らくLaTeXチームは「究極的には両者を統一したい」と考えているようにも思える。今は既存のパッケージの問題があって実現できていないようであるが、将来的には何か手を打つのがもしれない。

まとめ

とりあえず今のところは、ざんねん🙃(ざんねん🙃)


  1. 「文書プロパティ」はLaTeXの概念であり、PDFの「文書のプロパティ」とは無関係である。
  2. 相互参照を利用しているので、文書のビルドの際には2回タイプセットが必要である。以降の例の文書でも同様。
  3. 話を簡単にするため、当該のカウンタ(とページカウンタ)の表示書式は算用数字であることを仮定する。つまり、カウンタ番号のトークン列はカウンタ値の整数として通用する。
  4. 主に「\ref\pagerefの『実装において展開可能として動作する実行パス』を利用する」という方法と「相互参照情報を保存するマクロr@‹ラベル›を直接操作する」という2つの方法がある。
  5. hyperref等のパッケージの読込によって実装が置き換わる。また、最近のLaTeXの改修でも相互参照周りの実装が変動していて、従来の実装が動作しないという不具合が発生している。
  6. 定義する命令が\NewPropertyで、記録する命令が\RecoedProperties