マクロツイーター

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

TeX での「代数的データ型」の扱い (1)

または、「car/cdr と対応する cons を作る件について」。

もちろん、「car/cdr と対応する cons」の「仕様」は色々と考えられる。TeX で「リスト」を表す場合、通常は「Cons 対」を構成するのではなく、グループを並べた表現(「{one}{two}{three}...」の類)やデリミタを用いた表現(「one,two,three,...」の類)が用いられることを考えると、「(指定の表現形式の)リストの先頭と後続部」という扱いをするのが普通だろう。*1しかし今回は、敢えてリストを Cons で表して、「Cons に対応する car と cdr」を作ってみる。その理由は、この実装を「TeX における(似非)『代数的データ型』の扱い」という(少し変態的な)コーディングの事例としたいからである。

代数的データ型とは

……何だろうね。取り敢えず Wikipedia を参照。

代数的データ型(英: Algebraic data type)とはプログラミングにおけるデータ型の一種で、他のデータ型を当該代数的データ型のコンストラクタで包んだデータを値とする。この包まれたデータはコンストラクタの引数である。他のデータ型と異なり、コンストラクタが実行されることはなく、データを操作するにはパターンマッチを使ってコンストラクタからデータを抜き出さねばならない。

私自身、これくらいの意味合いで使っている。リスト型(α list; α は型変数)は次の 2 種類の構築子(=コンストラクタ)で表される。

  • Nil : α list; 空リストを表す。
  • Cons(x, y) : α × α list → α list; リスト y の先頭に x を追加したリストを表す。

ただし、これが正しく「リスト」(一列に並んだもの)の定義となるには、上掲の型制約が必要になる。しかし、TeX は(マクロの引数については)動的型付けの言語のように振る舞うので型検査は働かない。そのため、以下の実装における Cons は寧ろ Lisp 系の cons に近いものとなる。

TeX での実装の基本的アイデア

基本的にコンストラクタの形式をそのまま「TeX のマクロの呼出」の形式で表す。つまり、名前空間*2xx とすると、以下のようにする。

  • Nil\xx@Nil であらわす。
  • Cons(x, y) を \xx@Cons{<x>}{<y>} であらわす。

ただし、ここで \xx@Nil\xx@Cons 等の制御綴は特定のマクロに結び付けられているのでないことに注意する。そうすると、リスト ['foo', 'bar']、すなわち Cons('foo', Cons('bar', Nil)) の表現は次のトークン列となる。

\xx@Cons{foo}{\xx@Cons{bar}{\xx@Nil}}

普通の言語での「このリストを『変数』に代入する」に相当する操作は、TeX では「変数としてのマクロ」を使って実現できる。ここで面白いのは、構築子のマクロを一時的に \relax に等置することで、厄介な「変数の展開」の問題が単純な \edef で済んでしまうということである。((リストで扱う「値」が文字列や整数定数のように「頑強」である必要があるが。整数レジスタ等を扱う場合は \edef での代入で \the を被せて数字列に展開しておくとよいだろう。))

\def\xx@listA{\xx@Cons{foo}{\xx@Cons{bar}{\xx@Nil}}}
\let\xx@Cons\relax \let\xx@Nil\relax % 一旦構築子を「凍結」する
\edef\xx@listB{\xx@Cons{gee}{\xx@listA}} % \edef で定義
% これで \xx@listB の一回展開が
% \xx@Cons{gee}{\xx@Cons{foo}{\xx@Cons{bar}{\xx@Nil}}}
% となる。\xx@Cons、\xx@Nil がそのまま保たれるのに注意。

次に、リスト型の値を引数に取るマクロをどう書くかを例を用いて説明する。例えば、「与えられたリストが空であるかをスイッチ \ifxx@empty に返す」マクロ \xx@isempty を実装する。*3ポイントは、TeX のマクロでの制御綴への束縛が動的であることを利用して、当該のマクロの実行時に、各構築子に「定義を与える」ということである。例えば、構築子が \xx@Cons である場合、その引数の値に関わらずリストは空ではない。これを次のような「\xx@Cons の定義」として表す。*4

\def\xx@Cons#1#2{\xx@emptyfalse}

同様に「\xx@Nil の定義」も与えられる。

\def\xx@Nil{\xx@emptytrue}

そこで、マクロ \xx@isempty の定義を「\xx@Cons\xx@Nil の定義を上のものにその場で変更して、後はリストの内容を展開する」というものにする。

% スイッチを定義
\newif\ifxx@empty
% \xx@isempty{<Consリスト>} : リストが空かを判定しスイッチ
%  \ifxx@empty に返す.
\def\xx@isempty#1{%
  \def\xx@Cons##1##2{\xx@emptyfalse}%
  \def\xx@Nil{\xx@emptytrue}%
  #1}

かなり奇妙なコードであるが、これでパターンマッチが実現できて、if 文が不要になっていることが解るであろう。

car と cdr の実装

全く同様にして car と cdr を以下のように実装できる。ただし、ここでは結果を代入する制御綴を固定ではなく引数で指定する形式にした。空リストに対する car/cdr は本来は適切なエラーを出すべきであるが、ここでは手抜きして単に構築子マクロを未定義状態にして済ませた。((従って、実際にそれを行うと「\xx@Nil が未定義」という TeX のエラーが出る。))

% plain TeX / TeX on LaTeX 共用
\catcode`\@=11 %------------------------
% \xx@freeze@Cons : 構築子トークンを展開不能にする。
%  \edef でデータを作るのに有用。
\def\xx@freeze@Cons{%
  \let\xx@Cons\relax \let\xx@Nil\relax}
% \xx@do@car\CS{<Cons対>} : Cons対のcarを\CSに代入。
\def\xx@do@car#1#2{%
  \def\xx@Cons##1##2{\def#1{##1}}%
  \let\xx@Nil\@undefined % エラーにする
  #2}
% \xx@do@car\CS{<Cons対>} : Cons対のcdrを\CSに代入。
\def\xx@do@cdr#1#2{%
  \def\xx@Cons##1##2{\def#1{##2}}%
  \let\xx@Nil\@undefined % エラーにする
  #2}
\catcode`\@=12 %------------------------

ただし、実際に Cons リストをパターンマッチで扱う言語*5の使用者なら判ると思うが、パターンマッチを行うとその段階で car 部と cdr 部を別々に引数に束縛できるので、実際には car や cdr を使うことはかなり少ない。この後で、より複雑な処理の例を示すが、そこでも \xx@do@car\xx@do@cdr は全く使われていない。

(続く)

*1:無論、リスト以外の構造ならまた別の表現になるだろうが、とにかく、Lisp 系のように「全てを Cons 対で表す」ことはしない。

*2:無論、TeX には「名前空間」の機能がないので、これは単なる制御綴の名前の接頭辞である。

*3:ここで述べる手法を用いた場合、原理的にマクロを完全展開可能にできないので、戻り値は「特定の変数に代入する」等の扱いをする必要がある。

*4:関数でなくマクロなので、この場合引数は全く「評価」されないという点にも注意。

*5:典型的には ML 系や Haskell だが、多くの Lisp 系言語もパターンマッチの機能をもつ。