マクロツイーター

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

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

…つまり、2011-08-31 の記事の問題 4 を完全展開可能なように作る件について。ただし expl3 を使用して。

これを行うに際して一番難しいと思われる問題は、「それをする真っ当な理由を構築すること」であろう。これについては賢明な読者への課題として残しておこう。

文字列形式について

元々の出題では、A と B を子とする節点は [A|B] のように [ ] で囲って表すとしていた。ところがこれでは、ネストした時に [[1|2][3|4]] のようになり、対応する括弧を自力で探す必要が生じ、非常に解析しづらくなる。後でこのことに気付いて、問題を訂正して {A|B} でもよいことにした。

囲みを { } にすると、自動的に TeX のグルーピングのネストの判定が効くのでこちらの方が扱いやすい。ただし、実はこの形式には「TeX の中級程度の人が陥りやすい罠」が潜んでいる。(これについては後述。)最初に原稿を書いた時には { } としていたのだが、これを避けようとして後で変えたのである。にしても [A|B] は非常にまずかった(何でこれにしたんだろう…)。まだ構築子表現 \Node{A}{B} の方がよかったのかも……。

そういう顛末なので、以下の回答では {A|B} の形式を用いることとする。

戻り値の扱いについて

元の問題は(当然)展開可能でない実装を想定しているので、各「関数」の戻り値の整数を \countResult に代入するという規約になっている。この記事では完全展開を前提とするので、戻り値に関する規約を「完全展開した結果が戻り値の整数の十進表記になる」に変更する。(つまり、\sumLeaves{{1|2}|3}6 に展開される。)

共通部品

問題は 3 つの小問からなり、木を再帰的に辿る点は同じであるが各節点においては各々別の処理を要求している。この共通の「木を(文字列を解析して)辿る」の処理を毎回書くのは明らかに非常に醜いので、この処理を部品化する必要がある。そこで次のように考えてみる。

  • \zrxxpz_do_tree:n {<Tの表現>} は以下を行う。*1
    • T が葉(つまり整数 n)の場合: \zrxxpz_leaf:n {<n>} を実行する。
    • T が内部節点 {A|B} の場合: \zrxxpz_node:nn {<Aの表現>}{<Bの表現>} を実行する。恐らくこの中で A と B に対して \zrxxpz_leaf:n再帰的に呼び出されるだろう。
  • 各小問ごとに \zrxxpz_leaf:n\zrxxpz_node:nn の定義は異なる。だから、各小問のメイン関数はこの 2 つの制御綴に適切な関数を代入(\cs_set_eq:NN)してから、\zrxxpz_do_leaf:n を与えられた木に対して実行すればよい。(何となく「代数的データ型」の話で述べた方法と似ている。)

残念ながら、この方法はこのままでは使えない。何故なら、明らかに「代入」を伴っている((\cs_set_eq:NN は本質的には \let プリミティブである。))ので、完全展開可能にならないからである。

ところが、ある工夫をするとこの問題が解決できる。例として、\sumLeaves のための \zrxxpz_leaf:n\zrxxpz_node:nn の定義が \zrxxpz_sum_leaves_at_leaf:n\zrxxpz_sum_leaves_at_node:nn だとすると、

\cs_set_eq:NN \zrxxpz_leaf:n \zrxxpz_sum_leaves_at_leaf:n
\cs_set_eq:NN \zrxxpz_node:nn \zrxxpz_sum_leaves_at_node:nn
\zrxxpz_do_tree:n {<T>}

のように関数を変数に代入するのではなく、

\zrxxpz_do_tree:NNn
  \zrxxpz_sum_leaves_at_leaf:n
  \zrxxpz_sum_leaves_at_node:nn
  {<T>}

のように引数に渡す形式にすればよいのである。(引数指定子 NNn が示すように、\zrxxpz_do_tree:NNn は 3 つの引数を取り、最初の 2 つは関数を表す制御綴である。)これで代入操作が不要となり、完全展開可能にできる余地が生まれた。もちろん、別の小問では、同じ \zrxxpz_do_tree:NNn を別の関数を引数にして呼べばよい。

(続く)

*1:「木 T の文字列表現」を単に「T の表現」と呼ぶ。