TeXでマクロを書いたときにハマった落とし穴と這い出し方

序文

テーブルトークRPGのシナリオを記述するにあたって、ツールとしてTeXを使うことにした。これに伴って初めてTeXのマクロを記述することになったのだが、予想以上に散々引っかかって痛い目にあった。この記事は更なるTeXの犠牲者を出してはならないという強い決意を示すために記すものであり、また近い将来に自分がTeXの毒牙に掛かりそうになった時のための覚え書きでもある。

目的/方針

テーブルトークにおいて、ある行動の成否を判定する際にサイコロを振り、出目とキャラクターの能力値を比較して結果を決定するという操作を多く行う。これについての記述を簡略化するマクロ - judge環境を作りたい。


具体的には、

\begin{judge}{能力名}
 \item 結果A
 \item 結果B
\end{judge}

と記述すると

■判定 - 能力名

 成功 
      結果A
 失敗 
      結果B

のように表示させるマクロを実装することを目標とする。

judge環境の作成

begin-endの対応で記述する命令(これを「環境」と呼ぶ)を新たに作成するには、\newenvironment命令を使用する。judge環境を例とした使い方は以下のとおり。

\newenvoironment{judge}[1]{\beginを置き換える文字列}{\endを置き換える文字列}

カギ括弧で括られた1は引数の数であり、マクロ中ではn番目の引数を#nのように参照する。


まずは\begin句を置き換える部分の記述である。上から数えた\itemの番号で表記を変えたいので、ここではenumerate環境をベースに実装を行うことにする。

とりあえずシンプルに以下のように書いてみる。パーセント記号は改行文字をコメントアウトするために必要らしいので、適宜読み飛ばして欲しい。

\newenvironment{judge}[1]%
{\begin{enumerate}}%
{\end{enumerate}}

これでenumerate環境と同等の挙動をするjudge環境が作成された。これに、「■判定 - 能力名」を表示させる機能を付加する。ここでは\paragraphを用いて表示を行うことにする。

\newenvironment{judge}[1]%
{\paragraph{判定 - #1}%
 \begin{enumerate}%
}%
{\end{enumerate}}

これでenumerate環境の上にキャプションのような形で判定についての説明が表示されるようになった。

enumerate環境の改造

このままでは、各itemの前に表示されるのは通常のenumerate環境同様にインデックス番号である。これを任意の文字列に変更するためには以下の様な命令を用いる。\defは\newcommandと同様に新しい命令を定義する命令である。この二つの違いはいくつかあるが、ここでは割愛する。

\def\labelenumi{任意の文字列}

\labelenumiは、enumerate環境の1段目のラベルに対応する命令である。入れ子にして段数を深くしていく際は\labelenumii、\labelenmuiiiのように最後のiを重ねていく。また、この「任意の文字列中」では\theenumiという命令でインデックス番号が参照できる。こちらについても同様に、最後のiの数が入れ子の段数に対応している。

たとえばenumerate環境の一段目のラベルを丸括弧付きのインデックス番号にしたい場合は以下のように記述すれば良い。

\def\labelenumi{(\theenumi )}

今回は\theenumiが1の時に「成功」、2の時に「失敗」と表示させたいので、条件分岐を行う必要がある。

条件分岐

TeXでは\ifなどの命令を用いて条件分岐を行うのだが、これは他のプログラミング言語と比較してあまりに貧弱であるため、パズルのような思考を求められる。幸い今回は単純な目標であるので、ある数値が奇数かどうかを判定する\ifodd命令を使うことにする。使い方は以下のとおり。

\ifodd 数値 奇数の場合の処理 \else 偶数の場合の処理 \fi

これをjudge環境のbeginを置き換える部分に記述すれば良い。視認性を上げるため、「成功」「失敗」の文字はタイプライタ体にした。

\newenvironment{judge}[1]%
{\paragraph{判定 - #1}%
 \def\labelenumi{\ifodd \theenumi \tt{成功} \else \tt{失敗} \fi}%
 \begin{enumerate}%
}%
{\end{enumerate}}

このままでは、このマクロが呼ばれたあとのenumerate環境がすべて壊れてしまうため、endを置き換える部分で元の定義に戻してやる必要がある。通常のスタイルのままなら、以下のようにすれば良いだろう。

\newenvironment{judge}[1]%
{%
 \paragraph{判定 - #1}%
 \def\labelenumi{\ifodd \theenumi \tt{成功} \else \tt{失敗} \fi}%
 \begin{enumerate}%
}%
{%
 \end{enumerate}%
 \def\labelenumi{\theenumi}%
}

itemの拡張

大体の機能は実装し終えたが、このままでは「成功」「失敗」のあとに直接文字列が続いてしまい、(個人的な好みを言えば)改行があったほうが見やすくなると思う。インデックス番号のあとに改行を入れる方法については少しググると出てくるので説明は割愛するが、概ね以下の様なやり方が主流であるらしい。

\item \mbox{}\\ 文字列

つまり、\item命令を「\item \mbox{}\\」という命令列で置き換えるようにすれば良い。既存の命令を置き換えるには\renewcommand命令を使用する。使い方は以下のとおり。気をつける点としては、\newenvironmentでは定義する環境にバックスラッシュなり円記号は必要なかったが、\renewcommandでは必要なので注意されたい。

\renewcommand{\item}{置き換え後の命令列}

この\renewcommandを使って\itemを次のように定義したとする。

\renewcommand{\item}{\item \mbox{}\\}

素直に考えればこれで動いて欲しいところだが、残念ながらこれは動かない。TeXのマクロが展開されるのはそのマクロが出現した時点なので、その時にはマクロ中の\itemの定義もすでに置き変わっている。TeXはこれを再帰的に展開しようとして、(処理系にもよるが)5000程度でスタックオーバーフローを引き起こすだろう。


これを避け、既存の命令それ自身を使ったマクロを同名で定義する際には、\letを用いる。これはある命令に別の名前をつける命令である。これを用いて元々も\itemの定義を保持しておき、マクロ本体の中ではこちらを使うようにすることで無限再帰を回避することができる。具体的には以下のように記述する。

\let\origitem\item
\renewcommand{\item}{\origitem \mbox{}\\}

これで\itemの定義を置き換えることができた。また、これを元に戻すには以下のようにする。

\renewcommand{\item}{\origitem}

まとめ

ここまでの内容をまとめると、judge環境の定義は以下のようになる。

\newenvironment{judge}[1]%
{%
 \paragraph{判定 - #1}%
 \let\origitem\item%
 \renewcommand{\item}{\origitem \mbox{}\\}%
 \def\labelenumi%
 {%
  \ifodd \theenumi \tt{成功} \else \tt{失敗} \fi %
 }%
 \begin{enumerate}%
}%
{%
 \end{enumerate}%
 \def\labelenumi{\theenumi}%
 \let\item\origitem%
}


また、この記事で扱った内容を以下にまとめる。

  • begin-endで記述する環境は\newenvironmentで定義する
  • 通常の命令は\defもしくは\newcommandで定義する
    • 定義する命令名にバックスラッシュをつけるのを忘れない
  • enumerate環境のインデックス番号のスタイルを変更するには\labelenumiを使う
    • iの数で入れ子の深さに対応させる
    • \theenumiで現在のインデックス番号を参照できる
  • 条件分岐命令を用いて条件分岐を行う
    • \ifoddを用いて、ある数が奇数かどうかで分岐できる
    • 条件が真の場合は直後の命令列が、偽の場合は\else以降の命令列が呼ばれる
    • \fiで終端する
  • 既存の命令を拡張するには\renewcommandを用いる
    • 元々の命令をマクロに組み込むには\letを使って別名で定義を保存しておく必要がある


長々と綴ってきたが、もし私と同じようにマクロを書こうとして行き詰まった人にとって、この記事が何かしらの役に立てるのならば幸いである。