マクロツイーター

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

expl3な言語で完全展開可能な関数(マクロ)を作る件について (8)

前回の続き)

誰得感満載のこのシリーズがいつの間にか 8 回目になってしまった。しかし、今回が最終回なので安心してほしい(謎)。

関数 \zrxxnz_tail:n ― 正しい cdr の作り方

前回で保留になっていた関数 \zrxxnz_tail:n について述べることにする。

準備として、標準関数の \tl_head:n\tl_tail:n について解説する。これらは要するに、トークン列に対する car と cdr である。*1特に、トークン列がグループ({ } で囲まれたもの)を含まない場合は、厳密にその機能を果たす。(完全展開可能である。((ただし、\tl_head:n { \xx \yy \zz } のような場合に単純に完全展開を適用すると、\xx まで(可能ならば)展開されてしまう。これを避けたい場合のために \tl_head:w 〜 \q_stop という別形式が用意されている。基本形式より「気持ちの悪い」形である代わりに、これは 1 回展開で「正しい結果」に至ることが保証される。従って、\tl_head:w \xx \yy \zz \q_stop に「1 回展開」を適用することで確実に \xx を得られる。\zrxxnz_map_flatten:nN の実装でもこれを利用している。)))

\tl_head:n { feel } %(展開)=> f
\tl_tail:n { feel } %(展開)=> eel

しかし引数のトークンがグループを含む場合は少し事態が複雑になる。マニュアルの l3tl の部の冒頭の説明にあるように、l3tl *2の関数は、あるものはグループで囲まれたものを「1 つのもの」(アイテム(item)と呼ばれる*3)として扱い(例えば {42} は「1 つのアイテム」である)、あるものはグループを意識せずに本当に「トークンの列」として扱う({42} は「4 つのトークン」である)。tl_head:ntl_tail:n は基本的にアイテムを対象として機能する。

\tl_head:n { cei } { dei } { ghi } { ip } %(展開)=> cei
\tl_tail:n { cei } { dei } { ghi } { ip } %(展開)=> {dei}{ghi}{ip}

ここで tl_head:n の結果が {cei} でなく括弧が外れた形になることに注意しよう。これは妥当な仕様であろう。ところが実は、\tl_tail:n も「残りが 1 つしかない」場合には括弧が外れてしまうという困った仕様になっている。

\tl_tail:n { cei } { dei } %(展開)=> dei ; {dei} ではない!

引数リストの長さが 2 のときだけ括弧が外れるという全く一貫性のない動作は、tl_tail:n を「アイテム列」の操作に利用するにあたっては致命的な欠点といえる。何故こんな不可解な仕様になっているかは、この機能の実装を考えてみれば解るだろう。

\cs_new:Nn \tl_tail:n #1 {
    \tl_tail:w #1 \q_stop
}
\cs_new:Npn \tl_tail:w #1 #2 \q_stop {
    #2
}

恐らく多くの人がこのような「区切り付引数」を利用した定義を考えると思う。(実際の定義はより複雑だが、ここでの要点に関しては本質的に変わらない。)ところが、これでは #2 の部分が「括弧で囲まれた形」になっている場合は括弧が外れてしまう。これは「二分木の処理を完全展開可能にする(2)」の記事で述べた「区切り付引数の罠」そのものである。

恐らくこの \tl_tail:n の挙動は「仕様」であってバグではないのだろうが、何れにしてもこのままではアイテム列の処理には使えないので、「括弧が外れない cdr」を自分で実装してそれを用いた。「区切り付引数の罠」の回避は単純に済まないことが多くて悩ましい問題であるが、今の場合は次のように区切り付引数を使わない実装を用いて解決している。

\cs_new:Nn \zrxxnz_tail:n {
    \exp_after:wN \exp_after:wN \exp_after:wN \use:n
      \exp_after:wN { \use_none:n #1 }
}

まとめ

初級者は(ry


おまけ: Scheme 的 map-flatten

関数 \zrxxnz_map_flatten:nN を、引数の順番を保ったまま Scheme に翻訳したものを示す。ただし、TeX で「グループに入れること」と Scheme で「リストに入れること」は本質的に異なるので、全く同じ意味を持っているわけではない。

(define (map-flatten list fun)
    (map-flatten-aux1 '() (reverse xs) fun))
(define (map-flatten-aux1 rs xs fun)
    (if (null? xs) rs
        (map-flatten-aux2 (car xs) (cdr xs) fun rs)))
(define (map-flatten-aux2 x xs fun rs)
    (map-flatten-aux1 (append (fun x) rs) xs fun))

*1:Lisp の用語で、car は「リストの先頭要素」、cdr は「先頭を取り除いたリスト」を表す。

*2:expl3 の中のトークン列を扱うモジュール。

*3:この用語を利用して、「アイテム」の列と見做して処理されるトークン列のことを、私は「アイテム列」と呼んでいる。ただしこれは公式に使われている用語ではない。