マクロツイーター

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

二分木の処理を完全展開可能にする(2)

前回の続き)

共通部品の実装

それでは、前回に示した方針に従って、実際に \zrxxpz_do_tree:NNn を作ってみる。ただし事情があって名前は \zrxxpz_tree_dispatch:NNn に変更している。以下のコードで本質的なのは、最初の \zrxxpz_tree_dispatch:NNn (とその補助関数 4 つ)の定義のみで、残りはテスト用のコードである。

\documentclass{article}
\usepackage{xparse} % これで expl3 も読まれる
\ExplSyntaxOn  %------------------------

%% \zrxxpz_tree_dispatch:NNn \関数L \関数N {<二分木>}
% 葉ならば \関数L{<ラベル>}, 内部点なら \関数N{<左>}{<右>} に
% 展開される.
\cs_new:Npn \zrxxpz_tree_dispatch:NNn #1#2#3 {
    % この辺りはいつもの TeX のコーディング
    \zrxxpz_tree_dispatch_aux_i:w #3 \q_stop
    #1#2
}
  % ここで #1 の外側の括弧を故意に外している
\cs_new:Npn \zrxxpz_tree_dispatch_aux_i:w #1 \q_stop {
    \zrxxpz_tree_dispatch_aux_ii:ww #1 | \q_stop
}
\cs_new:Npn \zrxxpz_tree_dispatch_aux_ii:ww #1 | #2 \q_stop {
    \tl_if_empty:nTF {#2}
      { \zrxxpz_tree_dispatch_aux_iii:nNN {#1} }
        % ↓この場合 #2 は「<右> | 」
      { \zrxxpz_tree_dispatch_aux_iv:nwNN {#1} #2 }
}
\cs_new:Nn \zrxxpz_tree_dispatch_aux_iii:nNN {
    #2 {#1}
}
\cs_new:Npn \zrxxpz_tree_dispatch_aux_iv:nwNN #1#2 | #3#4 {
    #4 {#1} {#2}
}

%% (以下は \zrxxpz_tree_dispatch:NNn のテスト用のコード)

%%<*> \treeDispatchTest {<木>}
% \zrxxpz_tree_dispatch:NNn のテスト.
\cs_new:Npn \zrxxpz_dispatch_test:n {
    \zrxxpz_tree_dispatch:NNn
      \zrxxpz_test_leaf:n \zrxxpz_test_node:nn
}
% 内部節点の場合は Node<(左)><(右)> の形に展開する.
% (部分木はそれ以上解析せず元の文字列形式のまま.)
\cs_new:Nn \zrxxpz_test_node:nn {
    Node<#1><#2>
}
% 葉の場合は Leaf<(整数)> の形に展開する.
\cs_new:Nn \zrxxpz_test_leaf:n {
    Leaf<#1>
}
% (エクスポート)
\cs_new_eq:NN \treeDispatchTest \zrxxpz_dispatch_test:n

%%<*> \doAllTest \命令
% \do を \命令 と等価にして,  \testList を完全展開した結果を表示する.
\tl_new:N \zrxxpz_result_tl
\NewDocumentCommand \doAllTest { m } {
    \cs_set_eq:NN \do #1
    \iow_term:x { \testList }
}

%%<*> \testList: テストデータ(の集まり)
\tl_new:N \testList
\tl_set:Nn \testList {%
  \do8 ;
  \do{16} ;
  \do{8|9} ;
  \do{{8|9}} ;
  \do{{2|3}|5} ;
  \do{2|{4|5}} ;
  \do{{2|3}|{4|5}} ;
  \do{{{2|3}|{4|5}}} ;
  \do{{{{9|2}|6}|{4|{15|{2|4}}}}} ;
}

\ExplSyntaxOff %------------------------
% テスト実行
\doAllTest \treeDispatchTest

\begin{document}
\end{document}

テストの関数 \treeDiapatchTest の内部節点と葉のための下請けの関数は、両方とも、渡された引数の内容を含む単純な文字列に展開するようにしている。文書本体にある \doAllTest\treeDispatchTest が実行されると、トークンリスト \testList の内容を \do\treeDispatchTest に等置した状態で網羅展開した結果を端末に表示する。つまりそのリストにある 9 個の木の各々に対してテスト関数を適用した結果が順に表示されるわけである。実際の結果は以下のようになる。

Leaf<8>;Leaf<16>;Node<8><9>;Node<8><9>;Node<2|3><5>;Node<2><4|5>;Node<2|3><4|5>
;Node<2|3><4|5>;Node<{9|2}|6><4|{15|{2|4}}>;

実装に関する補足説明

前回の記事で、入力文字列形式を {A|B} と決めたが、これを厳密に守るならば、関数の呼び出しは \func{{{2|3}|5}} が正しいことになり、\func{{2|3}|5} では引数の文字列は「{2|3}|5」になってしまう。しかし、内部実装では寧ろ後者の形式(最外の括弧を外した形)を「正統な」形式として扱っている。これは TeX のマクロの引数の扱いの都合による。例えば、

\def\zrxx@func@aux#1|#2\@nil{ ... }

のような区切り付引数を利用したマクロ定義(敢えて expl3 でなく素の TeX で書いた)があって、これに

\zrxx@func@aux{2|3}|5\@nil

という引数を与えたとする。すると、#1 の内容は「2|3」となる。「{2|3}」ではないことに注意してほしい。区切り付引数にマッチするトークン列が丁度「1 つのグループ」になっていた場合は、TeX は最外の括弧を外したものを実際の引数とするのである。*1

普通に処理をすると勝手に外れてしまうので、外した形を前提としたわけである。ただし、\func{{{2|3}|5}} の形で呼び出しても正常に動作するように、「全体が括弧で囲まれていたらそれを外す」という処理を入れている(\zrxxpz_tree_dispatch_aux_i:w)。これを実現するのは容易である――つまり、先述の「癖」を逆に利用して、引数全体を一度区切り付引数の位置に置けばよいのである。

文字列か内部節点か葉かの判定は、その文字列(正確にはトークン列)が「グループ内でない〈|〉」を持つかで行う。これまでの説明で判るように、expl3 のスタイルで書いてはいるが、本質的には TeX で常用される「区切り付引数を利用したパターンマッチ」を忠実に実行している。先程例に出した \zrxx@func@aux を expl3 のスタイルで書くと以下のようになる。

\cs_new:Npn \zrxx_func_aux:ww #1 | #2 q_stop { ... }

q_stop は(LaTeX2e 内部コードの \@nil と同様に)「区切り付引数の終端」として慣習的に用いられるトークンである。ただし \@nil とは異なり、\ifx 判定でユニークとなるように定義が与えられている。具体的には、自分自身に展開されるマクロになっている。*2こういうトークンのことを expl3 では「クオーク(quark)」と呼んでいる。

(余談: こういう類の「いかにも TeX 的な」技巧は expl3 でも封印されている訳ではなく、いざと言う時には使用できる。\expandafter\exp_after:wN)も、\futurelet\peek_after:Nw((\peek_after:Nw\futurelet \l_peek_token と等しい。)))も、\aftergroup\group_insert_after:N)も揃っているので安心しよう(?))

*1:「区切り付引数が最外の括弧をもつことが稀にある」という場合、ここで括弧が外れてしまうことはなかなか気づきにくい。これは「区切り付引数」を操れるようになり始めた TeX の中級者を悩ませる罠といえよう。

*2:だから誤ってこれが展開される状態を引き起こすと無限ループになってしまう。