Help us understand the problem. What is going on with this article?

LaTeXの相互参照はいつでも解決(収束)するのか?

More than 3 years have passed since last update.

この記事は TeX & LaTeX Advent Calendar 2015 の14日目の記事です.昨日は aminophen さんでした.明日は okomok さんです.

TeX & LaTeX Advent Calendar 2015 も折返し地点を過ぎました.今年のテーマは「今さら人に聞けない、TeXのキホン」ですが,LaTeXの小ネタを投じます.

「なぜLaTeXを使うか?」というFAQがあれば必ずやエントリーするであろう機能に,相互参照があります.これは,文書内の節番号やページ番号,図表番号,数式番号などを自動で振るだけではなく,それらの番号を別の場所から指示したいときには(筆者が理解しやすい)ラベルを付けておいて,番号が必要なところではそのラベルを指定することで番号を自動で取り出して表示するという機能です.

使い方は簡単で,「Foo」節の節番号を取り出したければ \section{Foo} の直後に \label{sec:foo} を書き,利用したいところで \ref{sec:foo} と書きます.ここで,sec:foo は,文書内で一意になりそうな,分かりやすい名前を付けます.\pageref{sec:foo} とすれば,\label{sec:foo} に対応する場所のページ番号が取り出せます.

例えば以下のようなソースを書いたとしましょう.(簡単のため,pdflatex でタイプセットできるように和文を含めないようにしています.)

simple.tex
\documentclass{article}

\begin{document}
\section{Foo}\label{sec:foo}
TBA.
\newpage
\section{Bar}
See Section~\ref{sec:foo} (Page~\pageref{sec:foo}).
\end{document}

何回か(今の場合2回)タイプセットすると最終的に,2ページ目の本文で次のような出力が得られます.

See Section 1 (Page 1).

いま「何回か」と書きましたが,1回目が終わったときに確認すると,次のようになってしまいます.

See Section ?? (Page ??).

これは,処理速度(速くしたい)やメモリ使用量(少なくしたい)との兼合いから生じています.1回あたりのタイプセット処理を高速・省メモリで終えるため,1回の処理では,ソースファイルを最初から最後まで順に読み込みながら出力を行っては途中経過を忘れています.つまり,後になって振られた番号を前のほうに代入することは(実はその逆も)1回では適いません.ラベルと番号の対応付けを保存しておく中間ファイル(.aux 拡張子)の存在を仮定し,毎回の処理では前から順に対応付けを保存するとともに,前回の対応付けの中に対応するラベルがあれば番号を採用するということを行います.

タイプセットのログを確認すれば,1回目の処理では,

LaTeX Warning: Reference `sec:foo' on page 2 undefined on input line 8.

と警告が出ており,このメッセージに対応して出力に ?? が表示されます.2回目以降はこの警告がなくなり,正しい節番号とページ番号が出力されます.

さて,ここでクイズです.いかなる場合においても,2回のタイプセットで相互参照がすべて解決するでしょうか?

実際にある程度の長さの文章を書いたことのある人ならば,おおむね2回,ただしたまに3回必要なこともあるかもしれない,と答えることが多いでしょうか.経験則からすればそれで十分でしょう.2回で不十分なのは,?? の幅と,代入された参照番号の幅との違いから,改ページの場所や図表の場所が変わってしまう場合に起こりえます.

ラベルと番号の対応付けが変わってしまって,参照番号が正しく振られていないときには,タイプセットの最後のころに,次のような警告をもらうことになります.

LaTeX Warning: Label(s) may have changed. Rerun to get cross-references right.

この警告がなくなれば,相互参照が正しくできたと言えます.

そうとなれば,相互参照を解決するために必要な最小限のタイプセット回数を(LaTeXの外部においてでも)自動で判断して,その回数だけでタイプセットを打ち切りたいと考えたくなるのが人の常です.もっとも単純には,この警告の(部分)文字列が発見されたときはいつでもかつそのときに限り再タイプセットさせる,というアイディアが浮かびます.Boris Veytsman & Leila Akhmadeyeva (2013) には while ループを用いるスクリプト例が書いてあります.

では,ここで最後のクイズです.相互参照を解決するために必要なタイプセットの回数は常に有限でしょうか?

答えは否です.こういうときは,説明するより反例を一つ挙げてしまうのがもっとも説得力がありますね.以下が反例です.(pdflatex でタイプセットして確認しています.)

loop.tex
\documentclass{article}
\pagenumbering{roman}
\setlength{\textwidth}{5em}
\setlength{\textheight}{2\baselineskip}
\begin{document}
\setcounter{page}{999}
\noindent The Page Number \pageref{foo} is\label{foo} shaking up and down.
\end{document}

少々人工的な例ではありますが,\pageref{foo} がcmxcix(= 999)とm(= 1000)の間を行ったり来たりして,収束しません.相互参照の解決が不完全であったとしても,必ず停止してほしい場合(サーバ上で自動処理させる場合など)は,回数や時間を決めてループを止める処理を組み込んでおくのが安心ですね.

『[改訂第6版]LaTeX2ε美文書作成入門』(奥村晴彦・黒木裕介,技術評論社,2013)のp. 161には for ループを使って,最大4回で打止めにするシェルスクリプトの例が載っています(ステマ).

関連研究

公開後,関連研究について教えていただきましたので,追記します.この文書をself-containedにするために,コードを引用して示しておきます.

その1: @doraTeXさんの例

https://www.overleaf.com/read/hpxmwbcfpxdg より引用:

\documentclass{article}
\usepackage{lastpage}
\begin{document}
\makeatletter
\expandafter\let\expandafter\lastpage\csname r@LastPage\endcsname
\@tempcnta=1
\advance\@tempcnta\ifx\lastpage\relax0\else\expandafter\@secondoftwo\lastpage\fi
\@tempcntb=0
\loop
\advance\@tempcntb1
\the\c@page\newpage
\ifnum\@tempcntb<\@tempcnta\repeat
\makeatother
\end{document}

タイプセットするごとにページ数が増えていき,\lastpageの指す値が発散していって収束しません.

その2: @abenoriさんの例

http://math.sci.hokudai.ac.jp/~abenori/tmp/multcompiletest.tex より引用:

\documentclass{jsarticle}
\renewcommand{\thepage}{%
    \ifnum\value{page}=99九十九\else
    \ifnum\value{page}=100百\else\the\value{page}\fi\fi
}
\begin{document}
\setcounter{page}{99}
\vspace*{19.6cm}
\hspace*{16cm}
\pageref{a}
\section{a}\label{a}
\end{document}

本稿で提案した例とほぼ同じ発想です.\pageref の返す値が振動して,収束しません.ページ番号が,数値の意味で増えるにも関わらず文字列の長さが減る場合に,振動しそうだという着想を得るところが難しいと思っていたのですが,先行研究がありました.

その3: ZR さんの例

http://d.hatena.ne.jp/zrbabbler/20151217/1450344459 より引用:

\documentclass[a4paper]{jsarticle}
\usepackage[runs]{zref}
\begin{document}
\zruns 回目!
\end{document}

タイプセット回数をカウントアップしていくので,\zruns の値が発散します.よって,タイプセット結果は収束しません.ただしそれは意図通りの挙動であって,警告は出ません.

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした