「関数」のような関数の実現法
完全/制限付展開可能について注意すべきこととして、例え展開可能な関数であっても、それを(他の言語の「関数」のように)引数に入れて組み合わせたものは、必ずしも正しい動作にならないということである。これは、ただ「展開可能」だというだけでは「実際に展開される」とは限らないからである。このことを例を挙げて説明する。
今の \NabeAzzX
の実装について、それと「等価」な Scheme のプログラムを以前に本シリーズ (1) で示した。これを少し変更して次のようなロジックにしてみる。すなわち、(list-ec を使う代わりに)iota を使って (1 2 ... n)
というリストを先に作っておいて、それに変換関数を map することで目的のリストを得ている。この xxnz-x-main は以前のプログラムと同じ値を返す。
;(use srfi-1) (use srfi-13) (define (xxnz-x-main n) (map xxnz-process-step (iota n 1))) ; 残りは以前に挙げたコードと同じ (define (xxnz-process-step n) (if (xxnz-int-is-aho? n) `(AhoFont ,n) n)) (define (xxnz-int-is-aho? n) (or (= (modulo n 3) 0) (string-index (number->string n) #\3)))
iota の結果に相当するリストについては、\prg_stepwise_function:nnnN
で単純に {<整数10進表記>}
を出力する関数を反復させると一応作ることができる。
%% これ以降、特に明示する場合を除いて、\ExplSyntaxOn と \ExplSyntaxOff の %% 間のコードだけ記す。document の中は空である。 %% \zrxxnz_iota:n {<整数n>} % 1 から n までの整数がならんだアイテム列を生成する。 % (制限付展開可能) \cs_new:Nn \zrxxnz_iota:n { \prg_stepwise_function:nnnN { 1 } { 1 } {#1} \zrxxnz_iota_step:n } \cs_new:Nn \zrxxnz_iota_step:n { { #1 } } %% テスト: 網羅展開した結果を表示 \exp_args:Nx \tl_show:n { \zrxxnz_iota:n { 40 } }
テストの結果は以下のようになる。
> {1}{2}{3}{4}{5}{6}{7}{8}{9}{10}{11}{12}{13}{14}{15}{16}{17}{18}{19}{20}{21}{2 2}{23}{24}{25}{26}{27}{28}{29}{30}{31}{32}{33}{34}{35}{36}{37}{38}{39}{40}.
確かに望みの結果となっているが、これは網羅展開した結果であることに注意すべきである。上記のテストで \exp_args:Nx
を \exp_args:Nf
(完全展開)に変えると、やはり途中で展開が止まってしまう。*1
さて、この \zrxxnz_iota:n
が返すリストに対して、\tl_map_function:nN
(これも制限付展開可能)を適用すれば、前掲の Scheme のロジックを実現したことになる。全体のコードは以下の通り。
\documentclass{article} \usepackage{type1cm} \usepackage{xparse,l3str} \ExplSyntaxOn %------------------------ %% \zrxxnz_x_main:n {<整数>} % \NabeAzzX の実体. \cs_new:Nn \zrxxnz_x_main:n { % \exp_args:Nx % (*1) \tl_map_function:nN { \zrxxnz_iota:n { #1 } } \zrxxnz_process_step:n } %% 先ほど定義した関数。 %% \zrxxnz_iota:n {<整数n>} \cs_new:Nn \zrxxnz_iota:n { \prg_stepwise_function:nnnN { 1 } { 1 } {#1} \zrxxnz_iota_step:n } \cs_new:Nn \zrxxnz_iota_step:n { { #1 } } %% 以下の 2 つの関数の定義は以前の \NabeAzzX での %% ものと全く同じ。 %% \zrxxnz_process_step:n {<整数>} \cs_new:Nn \zrxxnz_process_step:n { \bool_if:nTF { \zrxxnz_int_is_aho_p:n {#1} } { { \AhoFont \int_to_arabic:n {#1} } } { { \int_to_arabic:n {#1} } } \c_space_tl } %% \zrxxnz_int_is_aho_p:n {<整数>} \cs_new:Nn \zrxxnz_int_is_aho_p:n { \bool_if_p:n { \int_compare_p:nNn { \int_mod:nn {#1} { 3 } } = { 0 } || \str_if_contains_char:nNTF { \int_to_arabic:n {#1} } 3 { \c_true_bool } { \c_false_bool } } } %%<*> \NabeAzzX {<整数>} \cs_new_eq:NN \NabeAzzX \zrxxnz_x_main:n \ExplSyntaxOff %------------------------ %%<*> \AhoFont \NewDocumentCommand \AhoFont {} {% \usefont{OT1}{cmfr}{m}{it}\LARGE } \begin{document} % 直接実行 \NabeAzzX{40} % 網羅展開した結果を表示 (*2) %\edef\result{\NabeAzzX{40}} %\show\result \end{document}
ところが、これを実際にコンパイルすると、エラーになってしまう。
! Missing number, treated as zero. <to be read again> \int_eval_end: l.62 \NabeAzzX{40}
何故失敗するのか、もう一度 \tl_map_function:nN
の呼び出し部分を確かめてみよう。
\tl_map_function:nN { \zrxxnz_iota:n { 40 } } \zrxxnz_process_step:n
\tl_map_function:nN
は第 1 引数のトークン列(アイテム列)を処理対象とする。ということは、ここでの処理対象のトークン列は \zrxxnz_iota:n {40}
である――そして、それを「展開」したものではない。これが意図した動作と異なることは明らかである。
実際に \tl_map_function:nN
に渡さなければいけないトークン列は、\zrxxnz_iota:n {40}
の「正しい結果」だったはずである。そして、\zrxxnz_iota:n
が制限付展開可能であることを考えると、渡された引数を網羅展開すればよい、ということが解る。つまり、上のソースリストの (*1) の行の \exp_args:Nx
を活かせばよい。実際にその修正を行うと正しく動作する。
すなわち、引数にある関数に「展開可能」の性質を与えておいて、それを利用した展開制御を併用することで、ようやく普通の言語の「関数」のような振舞い――引数の式が先に評価される――をさせることが可能となるのである。
*1:興味のある人はやってみよう。