LoginSignup
2

More than 3 years have passed since last update.

マクロとプリミティブの間の壁を打破せよ―LuaTeXで“1回展開”のマクロを定義する話

Last updated at Posted at 2019-12-24

これは「TeX & LaTeX Advent Caleandar 2019」の25日目の記事です。
(24日目は golden_lucky さん です。)

ご存じの通り、文書生成システムとしてのLaTeXの魅力の一つは「パッケージによりいくらでも機能を拡張できること」です。例えば、「文書にチョットだけ赤マフラーのゆきだるま:snowman:を入れたい」と思ったとすると、5年前であれば、「:snowman:の画像を用意する」「TikZなどで自分で:snowman:を描く」といった手間のかかる方法が必要でしたが、scsnowmanパッケージの登場によって、「パッケージを読み込んで\scsnowman[muffler=red]」だけで実現できるようになったわけですminisnowman.png

:sushi:※以降はTeX言語の話になります※:sushi:(えっ)

さて、そのLaTeXの拡張性の根源となっているのが「TeX言語のマクロ機能」です。一般にプログラム言語やソフトウェアにおける「マクロ機能」とは「複数の既存の機能の組合せに名前をつけて、あたかも単一の新しい機能であるかのように(つまり既存の機能と同じように)呼び出せる」機構のことを指し、「TeX言語のマクロ機能」もその一種といえます。つまり、LaTeXカーネルやscsnowmanパッケージのプログラムを読み込むことにより、あたかも「TeXに:snowman:を出す機能が加わった」と見なすことができるわけです。

しかし、改めて「TeX言語のマクロ」について「既存の機能(プリミティブ)と全く同じように呼び出せるか」という問題を考えると、プリミティブとマクロの間には展開回数という点で差があることがわかります。それは「(完全展開可能な)マクロの機能実行には最低2回の展開が必要である」というもので、TeX言語の上級者の間ではよく知られた事実です。

ところが、LuaTeXエンジンの拡張機能を使うと、この「2回展開の壁」を打破して、「1回展開で機能が実現できるマクロ」を実装することが可能になります。本記事では、「1回展開マクロ」の実装について説明します。

※以降では、「完全展開可能なマクロ」のみを考慮対象とします。

前提知識

というわけで、これはガチTeX言語者:sushi:向けの記事です。幸せなLaTeXユーザ:blush:の皆さんは「LuaTeXスゴイんだな~:astonished:」ということにしておいてAdvent Calendarのフィナーレを平和に待つことにましょう:christmas_tree::snowman:

この先を読みたいというTeX言語者については、以下の知識を仮定します。

何が問題なのかを確認してみる

※以降に挙げるコードは「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言語を頑張れば:muscle:なんとかなりそうです。やってみましょう。

先頭完全展開可能な実装
\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}

極めてアレ:sushi:なコードになってしまいましたが、とにかく実装できました。単に\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言語勢:muscle::muscle::muscle:であれば、「\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の「単純な実装」に適用すると以下のようになります。

expandedを利用した実装
\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

先頭完全展開にするためのアレ:sushi:な実装は不要で、それにも関わらず先の定義と同じ「2回展開」が実現できています。\expandedの威力、スゴイ:astonished:

長らくの間、\expandedをもつエンジンはLuaTeXだけでした。このままの状況であれば、ここで「LuaTeXスゴイ!:smiley:となってめでたく話が終わるところでした。しかし、expl3の実装上の要請のため、今年(2019年)に入って他の主要エンジン(pdfTeX・XeTeX・e-pTeX・e-upTeX)も\expandedプリミティブが実装されるようになりました。従って、TeX Liveの最新版においては、上記の\expandedを用いた実装は主要エンジンの全てで動作するようになっています。もはや\expandedだけでは「LuaTeXスゴイ!」とはいえないようです:upside_down:

1回展開は無理、アタリマエ

さて、ここまでTeX言語の力:muscle:を駆使して展開回数を「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を定義する」ための具体的な手順を説明します。

  1. LaTeXの補助マクロ\newluafunction8を使って新しい“Lua関数レジスタ”\xx@luaf@todayYMDを確保します。

    \newluafunction\xx@luaf@todayYMD
    
  2. ここで\luadefを使って、制御綴\todayYMDを「レジスタ\xx@luaf@todayYMDの関数呼出」と定義します。

    \luadef\todayYMD\xx@luaf@todayYMD
    
  3. 先ほど定義した関数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関数呼出”である)のですが、“マクロ機能”の一種であることは確かなので、細かいことは気にしないことにしましょう:upside_down:

xx.todayYMD()の定義の部分も含めて、以上のコードをまとめて整理したものを改めて載せておきます。

luadefを利用して「1回展開」を実現した実装
\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回展開」のマクロが実装できてしまいました:astonished:

まとめ

:blush:LuaTeXスゴイんだな~:astonished::christmas_tree::snowflake::gift::snowman:


  1. ご存じの通り、イマドキのLaTeXはe-TeX拡張を必須とするので、ここでもe-TeX拡張のエンジンを仮定します。 

  2. \year\month\dayレジスタには任意の整数が代入できるので、必ずしも“現在の”日付と一致するとは限りません。 

  3. ここでは「文字」は「カテゴリコード11または12の文字トークン」を指します。 

  4. 以降は「プリミティブ」は展開可能なもののみを指すことにします。(ちなみに、展開可能か展開不能かに関わらず、すべてのプリミティブは(先頭)完全展開可能であることに注意しましょう。) 

  5. 途中省略した\xx@todayYMD@bの部分の展開は\xx@todayYMD@aのそれとほぼ同じです。 

  6. \edefなどを使えば、定義文上は本体を2019/12/25以外にできる余地がありますが、ここで問題なのは定義されたマクロ自体の性質なので、その場合でも根本的な議論は変わりません。 

  7. LuaTeXの(昔からある)拡張プリミティブの\luafunctionを知っているならば「\luafunction<整数n>と等価となるトークンを\制御綴に代入する」という説明もできます。これは\countdef\制御綴<整数n>が「\count<整数n>と等価となるトークンを\制御綴に代入する」であることの類似物となっています。\luadefは代入文なので、\globalなどで修飾することも可能です。 

  8. 整数レジスタに対して\newcountがあるのと同様に、Lua関数レジスタに対して\newluafunctionが用意されています。 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
2