「関数」のような関数の実現法
完全/制限付展開可能について注意すべきこととして、例え展開可能な関数であっても、それを(他の言語の「関数」のように)引数に入れて組み合わせたものは、必ずしも正しい動作にならないということである。これは、ただ「展開可能」だというだけでは「実際に展開される」とは限らないからである。このことを例を挙げて説明する。
今の \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:興味のある人はやってみよう。