マクロツイーター

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

何か関数のようなもの (3)

前回の続き)

正しい方法その 3: 完全展開可能にする

シリーズ(1)において、\xx@remainder の実装で最後に単純に \the\xx@a を置くのは、結局戻り値を使う術がないから失敗すると述べた。特に

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

\xx@remainder の中に「展開不能なもの」があるため上手くいかないのであった。しかし、逆に言えば、もし仮に \xx@remainder が「展開可能なもの」しか含まずに戻り値の整数(の表記)まで辿りつけるとすれば、上掲の \edef は確かに \result に「16」を代入する*1ことになるだろうし、

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

という代入文も期待通りの動作(\@tempcnta に 16 を代入する)をするだろう。

以前の記事で書いたように、トークン列を「展開可能なトークンがある限り展開し続ける」ことを「完全展開」と呼び、マクロを「完全展開」して「正しい結果(この記事の文脈でいうと、戻り値を表すトークン列)」が得られることを「完全展開可能」という。以上に述べたことから、「関数」の役割を果たすマクロを作る方法の一つに、マクロを完全展開可能になるように実装することがあることが解る。

  • \xx@remainder{<整数1>}{<整数2>}
    完全展開した結果が、〈整数1〉を〈整数2〉で割った余りの 10 進表記となる。

では実際にその規約に従う \xx@remainder が作れるかというと、最初に挙げたロジックを使っている限りは無理である。何故なら、そのロジック自体が展開不能なもの(代入や \advance 等)を含んでいるからである。

ところが、\xx@in@five@digits の方は状況が異なる。

  • \xx@in@five@digits{<整数>}
    完全展開した結果が、〈整数〉の(0 補充の)10 進 5 桁の表記となる。

前回(シリーズ(2))で触れたように、

\expandafter\xx@in@five@digits@a\number<整数>\xx@nil
% ただし \xx@nil を展開不能にする

というトークン列を完全展開すると「戻り値」のトークン列そのものになる。ということは、以下の実装は完全に上記の要件を満たしているということになる。

\let\xx@nil\relax
\def\xx@in@five@digits#1{%
  \expandafter\xx@in@five@digits@a\number#1\xx@nil
}

これだけ見ると、シリーズ初回に挙げた \xx@remainder の失敗例と同じことをしているように見えるかも知れないので改めて確認しておく。確かに文書中で直接(他のマクロの引数の中ではなく)\xx@in@five@digits を書くと戻り値の文字列が文書に出力される。

%(本体中, \makeatletter 状態で)
\xx@in@five@digits{42}
%==>「00042」が出力される。

しかし、失敗例とは異なり、この実装は戻り値を変数に代入(文字列なので、マクロの \edef になる)することができる。また、\typeout\write で直接戻り値を出力させることも可能である。さらに、「動く引数」の中でも問題なく使える他、\thepage 等の書式マクロの中で使うことさえできる。

\edef\answer{\xx@in@five@digits{42}}
\typeout{\answer} %==>00042
% 端末に直接出力させる
\typeout{\xx@in@five@digits{42}} %==>00042
% 動く引数の中
\section{答えは\xx@in@five@digits{42}} % OK
% 書式マクロの中
\def\thepage{\xx@in@five@digits{\c@page}} % OK

つまり、マクロを「完全展開可能」になるように実装した場合に限って、「マクロが戻り値を吐く(どこかの変数に代入するのでなく)」という方法も使用可能になるのである。

「出力結果」と「戻り値」の違い

ここで補足しておきたいのは、ここの議論では「戻り値」とはプログラムの他の部分により「値」として読み込まれる可能性のあるものであって、「出力(組版)結果」は含まれないということである。*2

%% \xx@kintouwari{<幅>}{<文字列>} : 和文用の均等割り
\def\xx@kintouwari#1#2{%
  \hbox to #1{%
    \kanjiskip=0pt plus 1fil\relax
    \xkanjiskip=\kanjiskip
    #2}%
}

例えば、上のマクロはボックスを出力(組版)するのであり、「戻り値」といえるものは存在しない。従って展開可能である必要はない。実のところ、「失敗例の \xx@remainder」も、目的が本当に「余りの値を出力(組版)すること」であったならば正しいことになる。

そう考えると、結局初級者が間違えるのは、「出力結果」と「戻り値」を混同するのが原因と考えられる。「LaTeX のマクロ」で扱うような最も単純な形のマクロは大抵は展開可能である。

\newcommand*{\socuteis}[1]{#1かわいいよ#1}
\newcommand*{\xFloor}[1]{\lfloor#1\rfloor}
%%
\socuteis{Lua} %==>「LuaかわいいよLua」を出力
\tyoeout{\socuteis{Lua}} %==>LuaかわいいよLua

展開可能の場合は「LuaかわいいよLua」は「出力結果」とも「戻り値」とも解釈できる。だから LaTeX 学習(または TeX の初心者)の段階でこのような単純なマクロばかり扱っていると両者の区別を意識する機会がなくなり−−つまり「マクロがあたかも『関数』であるように見える」ようになり、後の段階で少々複雑なことをする場合にも「出力結果」がそのまま「戻り値」になるかのような錯覚を覚える。その辺りに原因があるのだろう。

それでも「初級者は完全展開可能に拘ってはいけない」

単純なマクロの性質(「出力結果」が「戻り値」になる)が複雑なマクロでは成立しなくなるのは、マクロが完全展開可能でないからだ、ということが解った。そうすると、なお一層マクロを完全展開可能に書きたい誘惑に駆られることであろう。だからこそ敢えて繰り返す。「初級者は完全展開可能に拘ってはいけない。」

私のブログの「完全展開」関連の記事を読んだ人であれば、「完全展開可能で実装する」ことがかなり技巧的なプログラミングを要することは既に判っていると思う。*3特に非 e-TeX では算術演算を完全展開で行う術がない*4そして重要なことは、「その 1」のような方法を使えば、完全展開可能にしなくても「関数のようなマクロ」は実現できる。確かに、「特定の変数に代入する規約」は格好悪いかも知れないが、とにかく簡単に使える方法があるのだからそれを使おう。

一番拙いのは、「変数に返すのは嫌だ、かといって完全展開可能で作れそうにもない、だから『関数』にするのを諦めてしまう」ことである。「プログラムの部品化」は理解しやすいコードを書くために欠かすことのできない概念である。それを先に言ったような本質的でない理由で避けることはあってはならないと思う。

*1:トークン列マクロ」の言葉を使った。

*2:組版結果」もボックスに入れる等の操作はできるが、少なくともこのシリーズで述べていることは通用しない。

*3:まずロジックを完全不能な操作を一切使わない形で組み立てる必要がある。その上で適切な展開制御を施す必要があり、自由度が少ない分この実現の何度も上がる。

*4:「算術演算」を自分で実装しない限り。