これは「TeX & LaTeX Advent Caleandar 2019」の25日目の記事です。
(24日目は golden_lucky さん です。)
ご存じの通り、文書生成システムとしてのLaTeXの魅力の一つは**「パッケージによりいくらでも機能を拡張できること」です。例えば、「文書にチョットだけ赤マフラーのゆきだるま**を入れたい」と思ったとすると、5年前であれば、「の画像を用意する」「TikZなどで自分でを描く」といった手間のかかる方法が必要でしたが、**scsnowmanパッケージ**の登場によって、「パッケージを読み込んで\scsnowman[muffler=red]
」だけで実現できるようになったわけです
※以降はTeX言語の話になります※(えっ)
さて、そのLaTeXの拡張性の根源となっているのが**「TeX言語のマクロ機能」です。一般にプログラム言語やソフトウェアにおける「マクロ機能」とは「複数の既存の機能の組合せに名前をつけて、あたかも単一の新しい機能であるかのように(つまり既存の機能と同じように**)呼び出せる」機構のことを指し、「TeX言語のマクロ機能」もその一種といえます。つまり、LaTeXカーネルやscsnowmanパッケージのプログラムを読み込むことにより、あたかも「TeXにを出す機能が加わった」と見なすことができるわけです。
しかし、改めて「TeX言語のマクロ」について「既存の機能(プリミティブ)と全く同じように呼び出せるか」という問題を考えると、プリミティブとマクロの間には展開回数という点で差があることがわかります。それは「(完全展開可能な)マクロの機能実行には最低2回の展開が必要である」というもので、TeX言語の上級者の間ではよく知られた事実です。
ところが、LuaTeXエンジンの拡張機能を使うと、この「2回展開の壁」を打破して、「1回展開で機能が実現できるマクロ」を実装することが可能になります。本記事では、「1回展開マクロ」の実装について説明します。
※以降では、「完全展開可能なマクロ」のみを考慮対象とします。
前提知識
というわけで、これはガチTeX言語者向けの記事です。幸せなLaTeXユーザの皆さんは「LuaTeXスゴイんだな~」ということにしておいてAdvent Calendarのフィナーレを平和に待つことにましょう
この先を読みたいというTeX言語者については、以下の知識を仮定します。
- TeX言語における展開制御全般。
- 特に「完全展開可能」「先頭完全展開可能」の概念。
(参考:\romannumeral の基本的な使い方(“関数”の合成))
何が問題なのかを確認してみる
※以降に挙げるコードは「LaTeX上の実行(エンジンはe-TeX拡張付き1のものであれば可)で\makeatletter
相当のカテゴリコード設定」の状態を仮定します。
例題として、以下のような命令を実装することを考えます。
-
\todayYMD
: その時点2の\year
・\month
・\day
レジスタの値を基にした、YYYY/MM/DD
形式の日付文字列3に展開される。(完全展開可能)
※\year
は4桁の正整数で\month
・\day
は有効な月日を表すことを前提とします。
“この記事の読者”であれば、いとも容易に実装できたことでしょう。
\def\todayYMD{%
\the\year/\two@digits\month/\two@digits\day}
※\two@digits
はLaTeXのカーネルで定義された補助マクロです。
確認してみましょう。
% \edef で完全展開した結果を端末に表示
\edef\TEST{\todayYMD}
\typeout{TEST=\TEST}%==>TEST=2019/12/25
% 日付の値を変えてみる
\year=2020 \month=2 \day=2 \edef\TEST{\todayYMD}
\typeout{TEST=\TEST}%==>TEST=2020/02/02
さて、これで機能は実現できたわけですが、ここで問題にしたいのは「展開回数」です。このマクロ\todayYMD
とプリミティブを、展開回数という点で比較してみましょう。
例えば、TeXのプリミティブ\jobname
について考えると、これは1回展開すると「結果」のトークン列(ジョブ名)が得られます。
\jobname
↓ (展開)
test
pdfTeX拡張のプリミティブ\pdffilesize
について考えると、やはり1回展開すると「結果」のトークン列(引数で与えたファイルのサイズの数字列)が得られます。
\pdffilesize {main.tex}
↓ (展開)
828
このように、プリミティブ4については「1回展開する」だけで「結果」のトークン列が得られるわけです。それでは、マクロ\todayYMD
についてはどうでしょうか。
\todayYMD
↓ (展開)
\the \year /\two@digits \month /\two@digits \day
このように、1回展開で得られるのは“途中結果”であって、「結果」のトークン列(2019/12/25
)になっていません。つまり、「展開回数」に関していうとこの\todayYMD
とプリミティブとの間には差異があるわけです。
\todayYMD
の展開をもう少し続けてみましょう。
\the \year /\two@digits \month /\two@digits \day
↓ (展開)
2019/\two@digits \month /\two@digits \day
これでトークン列の先頭が展開不能トークン(2
)になりましたが、まだ「結果」のトークン列には至っていません。つまり\todayYMD
は(完全展開可能ではあるが)先頭完全展開可能にすらなっていないのでした。一方でプリミティブは明らかに先頭完全展開可能であるので、これは大きな差異になってしまっています。
一般の「マクロ機能」におけるマクロは既存機能と同じように呼び出せることが望まれます。マクロとプリミティブの間にある「展開回数」に関する差異を取り除いて、1回展開で「結果」が得られるように\todayYMD
を実装するにはどうすればよいか、というのが本記事の本題です。
頑張って先頭完全展開可能にしてみる
とりあえず、\todayYMD
を先頭完全展開可能にするだけであれば、TeX言語を頑張ればなんとかなりそうです。やってみましょう。
\def\todayYMD{%
\expandafter\xx@todayYMD@a\number\year;}
\def\xx@todayYMD@a{%
\xx@two@digits\month\xx@todayYMD@b;}
\def\xx@todayYMD@b{%
\xx@two@digits\day\xx@todayYMD@c;}
\def\xx@todayYMD@c#1;#2;#3;{%
#3/#2/#1}
\def\xx@two@digits#1{%
\expandafter\xx@two@digits@a\number#1;}
\def\xx@two@digits@a#1;#2{%
\ifnum#1>9 \expandafter\@firstoftwo \fi #20#1}
極めてアレなコードになってしまいましたが、とにかく実装できました。単に\edef
で展開するだけでは先頭完全展開可能になっているかは判らないので、\todayYMD
の展開を自力で追ってみましょう5。
\todayYMD
↓ (展開)
\expandafter \xx@todayYMD@a \number \year ;
↓ (展開)
\xx@todayYMD@a 2019;
↓ (展開)
\xx@two@digits \month \xx@todayYMD@b ;2019;
↓ (展開)
\expandafter \xx@two@digits@a \number \month ;\xx@todayYMD@b ;2019;
↓ (展開)
\xx@two@digits@a 12;\xx@todayYMD@b ;2019;
↓ (展開)
\ifnum 12>9 \expandafter \@firstoftwo \fi \xx@todayYMD@b 012;2019;
↓ (展開)
\expandafter \@firstoftwo \fi \xx@todayYMD@b 012;2019;
↓ (展開)
\@firstoftwo \xx@todayYMD@b 012;2019;
↓ (展開)
\xx@todayYMD@b 12;2019;
↓
: (7回展開)
↓
\xx@todayYMD@c 25;12;2019;
↓ (展開)
2019/12/25
どうやら、先頭で17回展開すると「結果」が得られるようです。しかし、プリミティブの「1回展開」とはまだ大きな差があります。
もっと頑張って“加速”してみる
ガチTeX言語勢であれば、「\romannumeralトリック」を利用した**展開の“加速”**について知っていることでしょう。“加速”を利用して展開回数を減らしてみましょう。
展開の“加速”の原理は以下の通りです。
Sをトークン列とするとき、
\romannumeral-`>
S
の1回展開の結果はSの先頭完全展開になる。
※つまり、展開の“加速”を行うには先頭完全展開可能である必要があるわけです。
先に示した先頭完全展開可能な\todayYMD
の場合、大本の\todayYMD
の定義本体のトークン列の前に単純に\romannumeral-`>
を置けば十分です。
% \todayYMD の定義をこれに変える
\def\todayYMD{%
\romannumeral-`>\expandafter\xx@todayYMD@a\number\year;}
この\todayYMD
の展開過程は次のようになって、2回展開で「結果」が得られるはずです。
\todayYMD
↓ (展開)
\romannumeral -`>\expandafter \xx@todayYMD@a \number \year ;
↓ (展開 → \expandafter \xx@todayYMD@a …が先頭完全展開される
2019/12/25
2回展開の結果を確かめてみる
展開の“加速”というのは直観的には解りにくい概念なので、果たして本当に期待通りに展開が行われているかが不安に思うこともあるでしょう。TeXエンジンに「実際に2回だけ展開した結果」を出してもらいましょう。以下のようなマクロを用意します。
\def\xInspect#1{\typeout{%
once->\unexpanded\expandafter{#1}^^J%
twice->\unexpanded\expandafter\expandafter\expandafter{#1}^^J%
full->#1}}
xInspect
の引数に「展開結果を知りたいトークン列」を渡して実行すると、「1回展開」「2回展開」「完全展開」の結果が端末に表示されます。例えば、
\def\A{\B\B}\def\B{\C\C}\def\C{D}
\xInspect{\A}
を実行すると、以下の出力が得られます。
once->\B \B ←"\A"の1回展開は"\B \B"
twice->\C \C \B ←"\A"の2回展開は"\C \C \B"
full->DDDD ←"\A"の完全展開は"DDDD"
今までに取り扱った例について、\xInspect
で展開過程を確かめてみましょう。
\xInspect{\pdffilesize{main.tex}}
の実行結果:
once->828
twice->828
full->828
「単純な実装」の\xInspect{\todayYMD}
の実行結果:
once->\the \year /\two@digits \month /\two@digits \day
twice->2019/\two@digits \month /\two@digits \day
full->2019/12/25
「先頭完全展開可能な実装」の\xInspect{\todayYMD}
の実行結果:
once->\expandafter \xx@todayYMD@a \number \year ;
twice->\xx@todayYMD@a 2019;
full->2019/12/25
「展開を“加速”した実装」の\xInspect{\todayYMD}
の実行結果:
once->\romannumeral -`0\expandafter \xx@todayYMD@a \number \year ;
twice->2019/12/25
full->2019/12/25
展開を“加速”した実装では確かに「2回展開」が実現できていることが確かめられました。
チョット脱線:\expanded がチョットスゴイ話
展開制御に関わるLuaTeXの拡張プリミティブに\expanded
というのがあります。
-
\expanded{トークン列}
: これの1回展開の結果はトークン列
の完全展開となる。
\romannumeral
トリックによる“展開”の原理と似ていますが「先頭完全展開」ではなくて「完全展開」であるところが違います。つまり、\expanded
を使うと単なる「完全展開」のマクロであっても“加速”ができます。これを\todayYMD
の「単純な実装」に適用すると以下のようになります。
\def\todayYMD{%
\expanded{\the\year/\two@digits\month/\two@digits\day}}
\xInspect
の結果は以下の通りです。
once->\expanded {\the \year /\two@digits \month /\two@digits \day }
twice->2019/12/25
full->2019/12/25
先頭完全展開にするためのアレな実装は不要で、それにも関わらず先の定義と同じ**「2回展開」**が実現できています。\expanded
の威力、スゴイ
長らくの間、\expanded
をもつエンジンはLuaTeXだけでした。このままの状況であれば、ここで**「LuaTeXスゴイ!」**となってめでたく話が終わるところでした。しかし、expl3の実装上の要請のため、今年(2019年)に入って他の主要エンジン(pdfTeX・XeTeX・e-pTeX・e-upTeX)も\expanded
プリミティブが実装されるようになりました。従って、TeX Liveの最新版においては、上記の\expanded
を用いた実装は主要エンジンの全てで動作するようになっています。もはや\expanded
だけでは「LuaTeXスゴイ!」とはいえないようです
1回展開は無理、アタリマエ
さて、ここまでTeX言語の力を駆使して展開回数を「2回展開」までに削減できました。しかしプリミティブの「1回展開」との間にはまだ1回の差があります。この差を埋めてTeX言語で「1回展開」の\todayYND
を実装することはできるのでしょうか?
結論からいうと、それは不可能であり、しかもその理由は非常に簡単です。仮に、「1回展開」の\todayYND
マクロがあったとします。
\todayYMD
↓ (展開)
2019/12/25
この展開過程を満たそうとすると、\todayYMD
マクロの定義本体を2019/12/25
自体にする他ありません6。
\def\todayYMD{2019/12/25}
しかし、\todayYMD
の展開結果は\year
などのレジスタの値によって変化しなければいけないので、上の定義は\todayYMD
の仕様を満たしません。
要するに、TeX言語の力だけでは「1回展開」の\todayYND
は実現できないのでした。
今度は頑張ってLuaしてみる
※以降ではLuaTeXエンジンでの実行を前提とします。
LuaTeXを使っていて「TeX言語ではダメ」という話であれば、もうLuaを使うしかなさそうですね。というわけで、\todayYMD
の仕様を満たす出力をLuaで実装してみましょう。
\usepackage{luacode}
\begin{luacode*}
xx = {} -- モジュール
function xx.todayYMD()
tex.sprint(("%04d/%02d/%02d"):format(
tex.year, tex.month, tex.day))
end
\end{luacode*}
Luaの関数xx.todayYMD()
が実装できました。
\directlua だと結局ダメ
でも今作りたいのはTeXのマクロ\todayYMD
です。xx.todayYMD()
を呼び出すにはどうすればよいでしょう? \directlua
を使うのでしょうか?
\def\todayYMD{%
\directlua{xx.todayYMD()}}
もちろんこれは「普通のマクロの定義」の一種なので、「2回展開」にしかなりません。
once->\directlua {xx.todayYMD()}
twice->2019/12/25
full->2019/12/25
結局ほしいのは「\todayYMD
という制御綴の展開を直接xx.todayYMD()
の実行にする」ための機構、ということになるでしょう。
\luadef プリミティブ、スゴイ
実は、1.09版以降のLuaTeXには、まさにそういう機構が用意されています。
-
\luadef \制御綴 <整数n>
:\制御綴
の意味を「1回展開するとn番の“Lua関数レジスタ”に入っている関数を呼び出す」に設定する7。
この\luadef
を使うと何とかなりそうですね! というわけで、LuaLaTeX上で「\luadef
を利用して1回展開の\todayYMD
を定義する」ための具体的な手順を説明します。
-
LaTeXの補助マクロ
\newluafunction
8を使って新しい“Lua関数レジスタ”\xx@luaf@todayYMD
を確保します。\newluafunction\xx@luaf@todayYMD
-
ここで
\luadef
を使って、制御綴\todayYMD
を「レジスタ\xx@luaf@todayYMD
の関数呼出」と定義します。\luadef\todayYMD\xx@luaf@todayYMD
-
先ほど定義した関数
xx.todayYMD
をレジスタ\xx@luaf@todayYMD
に設定します。この操作はLua上で行うことになります。\begin{luacode*} -- Lua関数レジスタのテーブルを取得する local ft = lua.get_functions_table() -- \xx@luaf@todayYMD に対する番号を得る -- (luatexbase.registernumber はLaTeXカーネルで提供される) local rn = luatexbase.registernumber("xx@luaf@todayYMD") -- レジスタにLua関数を設定する ft[rn] = xx.todayYMD \end{luacode*}
これで「\todayYMD
を展開するとxx.todayYMD()
が呼び出される」という状態が実現できました。
※実際には\luadef
で定義されたトークンの意味はTeXマクロではない(“Lua関数呼出”である)のですが、“マクロ機能”の一種であることは確かなので、細かいことは気にしないことにしましょう
xx.todayYMD()
の定義の部分も含めて、以上のコードをまとめて整理したものを改めて載せておきます。
\usepakcage{luacode*}
\newluafunction\xx@luaf@todayYMD
\luadef\todayYMD\xx@luaf@todayYMD
\begin{luacode*}
local ft = lua.get_functions_table()
local rn = luatexbase.registernumber("xx@luaf@todayYMD")
ft[rn] = function()
tex.sprint(("%04d/%02d/%02d"):format(
tex.year, tex.month, tex.day))
end
\end{luacode*}
このコードを実行した上で、\xInspect{\todayYMD}
の結果を調べてみると……。
once->2019/12/25
twice->2019/12/25
full->2019/12/25
おお、スゴイ! 本当に**「1回展開」のマクロが実装できてしまいました**
まとめ
「LuaTeXスゴイんだな~」
-
ご存じの通り、イマドキのLaTeXはe-TeX拡張を必須とするので、ここでもe-TeX拡張のエンジンを仮定します。 ↩
-
\year
・\month
・\day
レジスタには任意の整数が代入できるので、必ずしも“現在の”日付と一致するとは限りません。 ↩ -
ここでは「文字」は「カテゴリコード11または12の文字トークン」を指します。 ↩
-
以降は「プリミティブ」は展開可能なもののみを指すことにします。(ちなみに、展開可能か展開不能かに関わらず、すべてのプリミティブは(先頭)完全展開可能であることに注意しましょう。) ↩
-
途中省略した
\xx@todayYMD@b
の部分の展開は\xx@todayYMD@a
のそれとほぼ同じです。 ↩ -
\edef
などを使えば、定義文上は本体を2019/12/25
以外にできる余地がありますが、ここで問題なのは定義されたマクロ自体の性質なので、その場合でも根本的な議論は変わりません。 ↩ -
LuaTeXの(昔からある)拡張プリミティブの
\luafunction
を知っているならば「\luafunction<整数n>
と等価となるトークンを\制御綴
に代入する」という説明もできます。これは\countdef\制御綴<整数n>
が「\count<整数n>
と等価となるトークンを\制御綴
に代入する」であることの類似物となっています。\luadef
は代入文なので、\global
などで修飾することも可能です。 ↩ -
整数レジスタに対して
\newcount
があるのと同様に、Lua関数レジスタに対して\newluafunction
が用意されています。 ↩