TeX Alchemist Online

TeX のこと,フォントのこと,Mac のこと

TeXで連想配列のようなものを実現するときの工夫

TeX で試験や問題集などの教材を作成するとき,他言語で言うところの連想配列(ハッシュテーブル,ディクショナリ)のようなものが欲しくなる点が多々あります。例えば,やりたいことをPythonで書くと,こういうコードを書くことに相当します。

questions = dict() # 問題文を記憶するディクショナリ
answers = dict() # 解答を記憶するディクショナリ
explanations = dict() # 解説文を記憶するディクショナリ

試験名 = ""
大問番号 = 0
小問番号 = 0

def 試験入力開始(_試験名):
    global 試験名, 大問番号, 小問番号
    試験名 = _試験名
    大問番号 = 0
    小問番号 = 0

def 大問開始():
    global 大問番号, 小問番号
    大問番号 += 1
    小問番号 = 0

def 問題インプット(問題文, 解答, 解説文):
    global 小問番号
    小問番号 += 1
    問題名 = f"{試験名}-第{大問番号}問-問{小問番号}"
    questions[問題名] = 問題文
    answers[問題名] = 解答
    explanations[問題名] = 解説文


# 入力作業開始
試験入力開始("期末試験")

大問開始() # 第1問のインプット開始

問題インプット("正しいものを選べ。", "(4)", "基本問題である。")
問題インプット("当てはまる値を答えよ。","42", "典型問題である。")
問題インプット("これの値はいくらになるか。","3.51", "難問であるが,前問と同様の発想で解ける。")

大問開始() # 第2問のインプット開始

問題インプット("不適切なものを1つ選べ。", "(ア)", "常識問題である。")
....

この入力作業により,

answers['期末試験-第1問-問2'] # => '42'

というような連想配列(ディクショナリ)が作成できるので,後はこれを加工して出力側の整形を行う,というような設計です。

\@namedef を使ったTeX言語への翻訳

上記コードを,TeX言語 (TeX on LaTeX) に翻訳することを考えます。連想配列の (key, value)(マクロの制御綴名, マクロ定義内容) で代用することにすると,次のようなコードになることでしょう。

\documentclass[uplatex,dvipdfmx]{jsarticle}
\usepackage{otf}

\makeatletter
\newcounter{大問番号}
\newcounter{小問番号}[大問番号]

\def\試験入力開始#1{%
  \def\試験名{#1}%
  \setcounter{大問番号}{0}%
  \setcounter{小問番号}{0}%
}

\def\大問開始{%
  \stepcounter{大問番号}%
}

\long\def\問題インプット#1#2#3{%
  \stepcounter{小問番号}%
  \def\問題名{\試験名-第\arabic{大問番号}問-問\arabic{小問番号}}%
  \@namedef{questions/\問題名}{#1}%
  \@namedef{answers/\問題名}{#2}%
  \@namedef{explanations/\問題名}{#3}%
}
\makeatother

\begin{document}

% 入力作業開始
\試験入力開始{期末試験}

\大問開始 % 第1問のインプット開始

\問題インプット{正しいものを選べ。}{\ajKakko{4}}{基本問題である。}
\問題インプット{当てはまる値を答えよ。}{$42$}{典型問題である。}
\問題インプット{これの値はいくらになるか。}{$3.51$}{難問であるが,前問と同様の発想で解ける。}

\大問開始 % 第2問のインプット開始

\問題インプット{不適切なものを1つ選べ。}{\ajKakkoKata{1}}{常識問題である。}
……

\end{document}

これにより,

\@nameuse{answers/期末試験-第1問-問2} 

$42$ へと展開されるようになり,Pythonコードにおけるディクショナリと同様の連想配列構造が実現されるようになります。

引数評価戦略の注意

単純なケースであればこれでよいのですが,実際の入力においては,解説文などの中で演算や変数参照を行いたい複雑なケースも出てきます。そのような場合,Pythonで書くにせよTeX言語で書くにせよ,引数の評価戦略 つまり「引数が評価されて最終的な値になるのはどのタイミングなのか」の意識が不可欠となります。

Pythonの場合

例えば Python のケースで,解説文中で試験名称や「前問」の問題番号に具体的に言及すべく,

# 問3の入力
問題インプット("これの値はいくらになるか。","3.51", f"{試験名}としては難問であるが,前問{小問番号-1}と同様の発想で解ける。")

と入力したとします。Pythonの引数は正格評価なので,この引数が評価されるのは 問題インプット という関数に渡される前となります。そのタイミングでは 小問番号 というグローバル変数はまだインクリメントされておらず,前問{小問番号-1} の部分は 前問1 となり,入力者の意図通り 前問2 とはなりません。

TeX言語の場合

逆に,TeX言語のケースで,

% 問3の入力
\問題インプット{これの値はいくらになるか。}{$3.51$}{\試験名 としては難問であるが,前問\the\numexpr \c@小問番号-1\relax と同様の発想で解ける。}

と入力したとします。この場合,“遅延評価”的な動きでマクロが展開されてゆく点に注意してマクロ展開を追いかけましょう。

\問題インプット を展開した中で,

\@namedef{explanations/\問題名}{\試験名 としては難問であるが,前問\the\numexpr \c@小問番号-1\relax と同様の発想で解ける。}

と展開されます。\@namedef の第1引数は \csname~\endcsname に展開され,この部分は“正格評価”的な動きをし,

explanations/\問題名 
→ explanations/\試験名-第\arabic{大問番号}問-問\arabic{小問番号} 
→ …… 
→ explanations/期末試験-第1問-問3

と完全展開されて,\explanations/期末試験-第1問-問3 に相当する制御綴の定義が行われます。

一方,\@namedef の第2引数は \def のマクロ定義部に展開され,そこは“遅延評価”的な発想で,この時点では展開されないので,\試験名 としては難問であるが,前問\the\numexpr \c@小問番号-1\relax と同様の発想で解ける。 というトークン列のまま保持され,\explanations/期末試験-第1問-問3 に相当するマクロの定義文となります。

すると,実際にこの“連想配列”を

\@nameuse{explanations/期末試験-第1問-問3}

として使用するとき,

\試験名 としては難問であるが,前問\the\numexpr \c@小問番号-1\relax と同様の発想で解ける。

と展開され,これはこの後(定義時点ではなく)使用時点での \試験名小問番号 の値へと展開されてしまいます。これでは意図通りとなりません。

\edef による完全展開で正格評価を再現

TeX言語で“連想配列もどき”を実装するとき,「Pythonのように定義時点での値に固定して定義したい」と思うのであれば,一つの解決策としては,\def ではなくて \edef を使うことでしょう。

\@namedef\edef 版は標準では用意されていないので,latex.ltx での定義を借用して定義しましょう。latex.ltx

\def\@namedef#1{\expandafter\def\csname #1\endcsname}

と定義されているので,これの \edef 版や(その \global 版にあたる)\xdef 版を次のように定義します。

\def\@nameedef#1{\expandafter\edef\csname #1\endcsname}
\def\@namexdef#1{\expandafter\xdef\csname #1\endcsname}

そして,これを用いて連想配列の定義部を

\@nameedef{explanations/\問題名}{#3}%

と変えれば,

\@nameedef{explanations/\問題名}{\試験名 としては難問であるが,前問\the\numexpr \c@小問番号-1\relax と同様の発想で解ける。}

の結果,\explanations/期末試験-第1問-問3 にあたるマクロは

macro:->期末試験にしては難問であるが,前問2と同様の発想で解ける。

で定義され,意図通りに「定義時点での値に固定されて定義される」動きとなります。

完全展開不可能な命令が含まれる場合への対応

ところが,上記コードにおいて,単純に \@namedef をその \edef 版にあたる \@nameedef に置き換えると,今度は「完全展開不可能な命令が与えられたときにコンパイルエラーを起こす」という新たな問題が生じます。

例えば,

\問題インプット{正しいものを選べ。}{\ajKakko{4}}{基本問題である。}

において,第2引数の部分が \edef 版で

\@nameedef{answers/\問題名}{\ajKakko{4}}%

で処理されてしまうと,\ajKakko の部分が完全展開できないので,コンパイルエラーを起こします。

\noexpand\unexpanded による解決策

そこで,完全展開できない命令を“連想配列”の値に用いたい場面では,\noexpand を前置するようにします。

\問題インプット{正しいものを選べ。}{\noexpand\ajKakko{4}}{基本問題である。}

これで,\edef に耐えるようになります。

あるいは,最近の e-TeX 拡張がなされているLaTeXエンジンならば,\unexpanded{...} で「\edef で展開されたくない部分(そのトークン列のままになって欲しい部分)を丸ごと範囲で指定」ということができます。

\問題インプット{正しいものを選べ。}{\unexpanded{\ajKakko{4}}}{基本問題である。}

保護付き完全展開とロバスト化による解決策

しかし,\ajKakko を使うたびに毎回 \noexpand\unexpanded{...} を伴わせるというのも面倒です。そこで,「保護付き完全展開」を用いてみましょう。latex.ltx の中で \protected@edef\protected@xdef として定義されているものです。これらの name~ 版を次のように用意します。

\def\protected@nameedef#1{\expandafter\protected@edef\csname #1\endcsname}
\def\protected@namexdef#1{\expandafter\protected@xdef\csname #1\endcsname}

そして,“連想配列”の定義部を,次のように \protected@nameedef を用いた形に変えます。

\protected@nameedef{questions/\問題名}{#1}%
\protected@nameedef{answers/\問題名}{#2}%
\protected@nameedef{explanations/\問題名}{#3}%

そして,etoolboxパッケージ\robustify を用いて,\ajKakko 自体をロバスト化しておくわけです。

\usepackage{etoolbox}
\robustify\ajKakko

すると,「保護付き完全展開」によって,\ajKakko という制御綴は展開されることなく,そのトークンを保ったまま“連想配列”の値として格納されることになります。このように,「保護付き完全展開」を活用することで,意図通りの“連想配列の再現”が可能となるわけです。

参考文献

zrbabbler.hatenablog.com

zrbabbler.hatenablog.com

zrbabbler.hatenablog.com

zrbabbler.hatenablog.com