マクロツイーター

はてダから移行した記事の表示が崩れてますが、そのうちに直せればいいのに(えっ)

TeX で何か関数のようなものを (1)

既にこのブログでは何度も話題に出ているように、TeX には、他の多くのプログラミング言語にあるような「関数」(ユーザ定義関数)の概念がない。*1この記事では「関数」に相当する機能をどうやって実現するかを解説する。

「関数」の一番大事な性質は、「戻り値がある」ということである。例えば、数学の関数 f(x,y) = 2x + y であれば、f(4, 2) には 10 という「戻り値」*2がある。TeX のマクロで関数のような機能を持たせようとすると問題になるのがこの「戻り値」の扱いである。*3従って、以下では「戻り値」の扱い方を中心に説明することになる。

2 つの例題

説明のために、以下の 2 つの単純な関数を例として用いる。関数であるから「戻り値」(「〜を返す」の部分)があることに注意しよう。((このような機能を公開命令(public)として提供することは恐らくないだろうから、内部命令(private)、すなわち @ の入った制御綴を用いる。))

  • \xx@remainder{<整数1>}{<整数2>}
    〈整数1〉を〈整数2〉で割った余りを返す。(〈整数1〉は非負、〈整数2〉は正であることを前提とする。)
  • \xx@in@five@digits{<整数>}
    〈整数〉を 10 進 5 桁(前に 0 を補う)の文字列で表したものを返す。(〈整数1〉は非負で 10 進 5 桁以内であることを前提とする。)

\xx@remainder は 2 つの整数を受け取り整数を返す。\xx@in@five@digits は 1 つの整数を受け取りトークン列(文字列)を返す。

ここで問題にするのは引数と戻り値の扱いであり、「関数の中」のロジックではないので、ロジックについては先に「答え」を与えてしまおう。\xx@remainder については、整数レジスタ \xx@a を宣言したうえで、次の一連のコードを実行すれば、\xx@a に「戻り値」が入る。

\xx@a=<整数1>\relax
\divide\xx@a<整数2>\relax
\multiply\xx@a-<整数2>\relax
\advance\xx@a<整数1>\relax

\xx@in@five@digits は少し技巧的になるが、次のようにしてできる。まず次の補助マクロを定義する。

% Lua で書くとこんな感じの処理
% function infivedigits(num)
%   return ((num.."/0000"):sub(1,6):gsub("^(.*)/(.*)$","%2%1"))
% end
\def\xx@in@five@digits@a#1\xx@nil{% #1 は10進表記
  \xx@in@five@digits@b#1/0000\xx@nil}
\def\xx@in@five@digits@b#1#2#3#4#5#6#7\xx@nil{%
  \xx@in@five@digits@c#1#2#3#4#5#6\xx@nil}
\def\xx@in@five@digits@c#1/#2\xx@nil{%
  #2#1}

その上で、

\xx@infivedigits@a<整数の10進表記>\xx@nil

を 2 回展開した結果のトークン列が「戻り値」と一致する。

関数の内部のコードは既に与えられたので、あとは「外部」とのやり取りをどうするかという問題だけが残っている。

失敗例

\xx@remainder の実装について、初級者がよくやる間違いは、次のようなコードである。

\def\xx@remainder#1#2{%
  \xx@a=#1\relax
  \divide\xx@a#2\relax
  \multiply\xx@a-#2\relax
  \advance\xx@a#1\relax
  % ここで \xx@a が余りの値である
  \the\xx@a % 余りを出力する
}

これで \xx@remainder{100}{42} を実行すると、確かに組版結果に正解の 16 が現れる。((恐らく、初級者が最初に書くのは、最後を単に 「...\xx@a}」にしたものかも知れない。(これは \xx@a に対する代入文の左辺だけを書いたことになるので有意義な動作をしない。)「16 が出力される」ようなものを作ろうと試行錯誤した結果、\the とか \number とかを使うことになるのだろう。))一体どこが間違っているのだろうか。

このコードの問題点は、実際にこの「関数」を使おうとすれば判る。余りを求める関数を定義する動機は何だろうか? 恐らくは「余りの値を文書に出力するため」ではないであろう。余りの値を後の処理で用いたいはずである。では、\xx@remainder{100}{42} の戻り値(つまり 16)を \@tempcnta に代入するにはどうすればよいか。

\@tempcnta=\xx@remainder{100}{42}

これはダメである。何故なら、実際に \xx@remainder のマクロを展開すると、

\@tempcnta=\xx@a=100\relax\divide\xx@a42\relax...\the\xx@a

という支離滅裂なコード((\@tempcnta に(現在の)\xx@a の値を代入して、「=100」を文書に出力する、……という動作になる。))になってしまう。

\edef で一旦全部展開してしまえば、展開結果が「16」になって、その後の処理ができる、と考えた人もいるかも知れない。しかし、残念ながらそれも上手くいかない。代入文や \advance 等の演算は「展開されない」からである。実際、

\edef\xx@temp{\xx@remainder{100}{42}}

を実行すると、\xx@temp の展開テキストは以下のようになる。

\xx@a=100\relax\divide\xx@a42\relax ... \advance\xx@a100\relax123

ここで「123」は \edef 実行時点での \xx@a の値である。そもそも、代入文や \advance は「状態を変える」(この場合レジスタの値を更新する)という作用を持つが、これらは「実行」されて初めて有効となるので、「展開」だけでは扱えない(だから「展開」の対象になっていない)のである。内部のロジックで「展開不能」なものを使っている以上、\edef は使えないので一旦忘れよう。

*1:大昔の 8 ビットパソコンで稼動していた BASIC 言語にはユーザ定義関数は(単一の式での定義を除き)なかった。COBOL の標準仕様にユーザ定義関数が加わったのは 21 世紀になってかららしい。

*2:「戻り値(return value)」は手続き型言語の用語であり、数学や関数型言語では単に「値(value)」と呼ぶ。

*3:言語によっては「戻り値のない手続き」も戻り値のあるものと統一的に扱うために「関数」としているものがあるが、ここで述べた理由により、このような「関数」は考えないことにする。言語によっては「関数」が副作用を持ったり引数以外の「状態」を参照することがあるが、これもここでの話題とは無関係である。