以下の内容はhttps://zrbabbler.hatenablog.com/entry/20120218/1329587100より取得しました。


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

前回の続き)

出題の 3 つの関数の実装

前回に作った補助関数 \zrxxpz_tree_dispatch:NNn を用いて、問題で求められている 3 つの関数(\sumLeaves\treeDepth\sumLeavesWithin*1)を実装することにする。前回のコードと同じ部分がかなりあるが敢えて完全なソースリストを掲載する。

\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn  %------------------------

%%<*> \sumLeaves {<木>}
\cs_new:Nn \zrxxpz_sum_leaves:n {
    \int_value:w \int_eval:w 0
    \zrxxpz_sum_leaves_aux:n {#1}
    \int_eval_end:
}
\cs_new:Npn \zrxxpz_sum_leaves_aux:n {
    \zrxxpz_tree_dispatch:NNn
      \zrxxpz_sum_leaves_at_leaf:n \zrxxpz_sum_leaves_at_node:nn
}
\cs_new:Nn \zrxxpz_sum_leaves_at_leaf:n {
    + #1
}
\cs_new:Nn \zrxxpz_sum_leaves_at_node:nn {
    \zrxxpz_sum_leaves_aux:n {#1}
    \zrxxpz_sum_leaves_aux:n {#2}
}
\cs_new_eq:NN \sumLeaves \zrxxpz_sum_leaves:n

%%<*> \treeDepth {<木>}
\cs_new:Nn \zrxxpz_tree_depth:n {
    \int_value:w \int_eval:w
    \zrxxpz_tree_depth_aux:n {#1}
    \int_eval_end:
}
\cs_new:Npn \zrxxpz_tree_depth_aux:n {
    \zrxxpz_tree_dispatch:NNn
      \zrxxpz_tree_depth_at_leaf:n \zrxxpz_tree_depth_at_node:nn
}
\cs_new:Nn \zrxxpz_tree_depth_at_leaf:n {
    \c_zero
}
\cs_new:Nn \zrxxpz_tree_depth_at_node:nn {
    \zrxxpz_int_max:nn
      { \zrxxpz_tree_depth_aux:n {#1} }
      { \zrxxpz_tree_depth_aux:n {#2} }
    + 1
}
\cs_new_eq:NN \treeDepth \zrxxpz_tree_depth:n

%% \zrxxpz_int_max:nn {<整数式1>} {<整数式2>}
% 式の評価を一度しかしない \int_max:nn.
\cs_new:Nn \zrxxpz_int_max:nn {
    \exp_args:Noo \int_max:nn
      { \int_value:w \int_eval:w #1 \int_eval_end: }
      { \int_value:w \int_eval:w #2 \int_eval_end: }
}

%%<*> \sumLeavesWithin {<木>} {<深さ>}
\cs_new:Nn \zrxxpz_sum_leaves_within:nn {
    \int_value:w \int_eval:w 0
    \zrxxpz_sum_leaves_within_aux:nn {#1} {#2}
    \int_eval_end:
}
\cs_new:Npn \zrxxpz_sum_leaves_within_aux:nn { % {<木>}{<深さ>}
    % tree_dispatch は次の引数の <木> までを処理し,
    % その次の <深さ> には手を付けない.
    % 従って, <深さ> は sum_leaves_within_at_leaf/node に
    % そのまま渡される.
    \zrxxpz_tree_dispatch:NNn
      \zrxxpz_sum_leaves_within_at_leaf:nn
      \zrxxpz_sum_leaves_within_at_node:nnn
}
\cs_new:Nn \zrxxpz_sum_leaves_within_at_leaf:nn { % {<ラベル>} {<深さ>}
    + #1
}
\cs_new:Nn \zrxxpz_sum_leaves_within_at_node:nnn { % {<左>} {<右>} {<深さ>}
    \int_compare:nNnT {#3} > { 0 } {
        \exp_args:Nnno \zrxxpz_sum_leaves_within_at_node_aux:nnn {#1} {#2}
          { \int_value:w \int_eval:w #3 - 1 \int_eval_end: }
    }
}
\cs_new:Nn \zrxxpz_sum_leaves_within_at_node_aux:nnn {
    \zrxxpz_sum_leaves_within_aux:nn {#1} {#3}
    \zrxxpz_sum_leaves_within_aux:nn {#2} {#3}
}
\cs_new_eq:NN \sumLeavesWithin \zrxxpz_sum_leaves_within:nn

%%<*> \sumLeavesWithinOne {<木>}
% \sumLeavesWithin で第 2 引数を 1 に固定したもの.
\cs_new:Npn \sumLeavesWithinOne #1 {
    \sumLeavesWithin {#1} { 1 }
}

%% \testSumLeavesWithin {<木>}
% 与えられた木に対して, 深さ 0, 1, 2, 3, 4 の各々に対して
% \sumLeavesWithin を適用した結果を順に端末に表示する.
\NewDocumentCommand \testSumLeavesWithin { m } {
    \cs_set:Nn \zrxxpz_test_sum_leaves_within_aux:n {
        \sumLeavesWithin {#1} {##1}
        ;
    }
    \iow_term:x {
        \prg_stepwise_function:nnnN { 0 } { 1 } { 4 }
          \zrxxpz_test_sum_leaves_within_aux:n
    }
}
\cs_new:Nn \zrxxpz_test_sum_leaves_within_aux:n {}

%%%% ↓以下は前回までに示したコードと同じ↓

%% \zrxxpz_tree_dispatch:NNn \関数L \関数N {<二分木>}
% 葉ならば \関数L{<ラベル>}, 内部点なら \関数N{<左>}{<右>} に展開される.
\cs_new:Npn \zrxxpz_tree_dispatch:NNn #1#2#3 {
    \zrxxpz_tree_dispatch_aux_i:w #3 \q_stop
    #1#2
}
\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} }
      { \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}
}

%%<*> \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)
\doAllTest\sumLeaves
\doAllTest\treeDepth
\doAllTest\sumLeavesWithinOne
% テスト実行(\testSumLeavesWithin)
\testSumLeavesWithin{{{{9|2}|6}|{4|{15|{2|4}}}}}
\begin{document}

\end{document}

テストでは、以下の結果を端末に出力させている。

  • \testList の各々の木 T について、\sumLeaves{T} の値。
  • \testList の各々の木 T について、\treeDepth{T} の値。
  • \testList の各々の木 T について、\sumLeavesWithin{T}{1} の値。
  • 木 T = {{{9|2}|6}|{4|{15|{2|4}}}} と深さ d = 0, 1, 2, 3, 4 について、\sumLeavesWithin{T}{d} の値。

なお、端末への出力には \iow_term:x という標準関数を用いている。指定子 x から判るように、引数のトークン列は網羅展開される(その過程で \sumLeaves 等も網羅展開されている)。ちなみに、\iow_term:x は LaTeX2e の \typeout(これも引数を網羅展開するのであった)と機能的に同値であるが、expl3 には「展開しない」版の \iow_term:n も存在する。

出力結果は以下の通り。

8;16;17;17;10;11;14;14;42;
0;0;1;1;2;2;2;2;4;
8;16;17;17;5;2;0;0;0;
0;0;10;36;42;

実装のポイント

上掲の 3 つの関数(の本体)の実装はどれも以下のような形になっている。

\int_value:w \int_eval:w <整数式> \int_eval_end:

これを展開すると、与えられた式の値(の十進表記)に展開される。(ちなみに生の TeX で書くと \number\numexpr<整数式>\relax である。)この「式」の部分には式(の一部)に展開されるような関数を置くことも可能であり、従って、「適当な式文字列」を生成する展開可能関数(制限付展開可能でも可)を構成すればよい。例えば、\sumLeaves では「式」は 0 \zrxxpz_sum_leaves_aux:n {<木>} である。この \zrxxpz_sum_leaves_aux:n は実は以下のような単純なことしかしていないが、これで所望の結果が得られるのである。

\zrxxpz_sum_leaves_aux:n {{{9|2}|6}|{4|{15|{2|4}}}}
→ +9+2+6+4+15+2+4

なお、上掲のテストでは制限付展開可能性しか検査していない(網羅展開しているので)が、実際には完全展開可能である。上記の \int_value:w 〜 の構成では、先頭の \int_value:w を 1 回展開するとそれだけで最後の \int_eval_end: を見つけるまで途中にある展開可能トークンを展開し続けるからである。*2(複数の展開が行われるがそれが全体で「1 回の展開」と見做されることに注意。)\sumLeaves の定義本体の先頭が \int_value:w であるので、これから \sumLeaves が「完全展開可能」であることが導かれる。

関数 \zrxxpz_int_max:nn について

小問 2 の \treeDepth では途中で「2 つの整数の最大値」を求める箇所がある。この機能を与える標準関数 \int_max:nn は完全展開可能であるのでこれを使えそうにみえる。ところが、この関数には、引数の整数式を 2 回評価するという欠点がある。*3普通はそれでもあまり問題にならないだろうが、この問題の場合、引数が \zrxxpz_tree_depth_aux:n再帰呼出になっているので、2 回評価されると不必要なコストがかかってしまう。これを避けるために、引数を先に評価する版である \zrxxpz_int_max:nn を定義して用いている。

\cs_new:Nn \zrxxpz_int_max:nn {
    \exp_args:Noo \int_max:nn
      { \int_value:w \int_eval:w #1 \int_eval_end: }
      { \int_value:w \int_eval:w #2 \int_eval_end: }
}

見ての通り、与えられた整数式を先述の \int_value:w 〜 の構成の中に入れて、\exp_args:〜 を利用してそれを 1 回展開してから、標準の \int_max:nn にその結果(単なる数字になっている)を渡している。「1 回展開」で十分な理由も既に述べた。

まとめ

expl3 の枠組でも、関数を完全展開可能にするのは極めて大変である。

従って、expl3 の初級者は関数を完全展開可能にすることに拘ってはいけない。絶対。

*「えー!? またそれですかー? というか、お前も(expl3 の)初級者ではないのかと小一時間\<(ry」

*1:ただし完全展開可能にするため、戻り値の規約を「値の十進表記に展開する」に変更した。

*2:ただし、「式」(の展開結果)が正しい書式を持っているという前提が付く。

*3:将来の expl3 では改善されているかも知れない。




以上の内容はhttps://zrbabbler.hatenablog.com/entry/20120218/1329587100より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14