マクロツイーター

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

expl3な言語で完全展開可能な関数(マクロ)を作る件について (5)

「関数」のような関数の実現法

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

今の \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:興味のある人はやってみよう。