マクロツイーター

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

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

前回の続き)

「展開可能のジレンマ」から抜け出す

シリーズ(5)(6)において、「iota で生成した整数列を map に通す」というロジックを expl3 で実装しようとしたが、そうすると「完全展開可能」と「制限付展開可能」の差異のせいで全体を展開可能にすることが困難になるという話をした。失敗したまま話を終えるのは気が引けるので、ここでは解決策を探ることにしよう。

そもそも(全体を展開可能にしようと試みた時に)行き詰まってしまうのは、\zrxxnz_iota:n が「完全展開可能」でなくて「制限付展開可能」であるからだった。ならば、この関数を「完全展開可能」になるように実装を変えれば(つまり \prg_stepwise_function:nnnN に頼らず自分で全て実装すれば)よいことになる。

どうせ実装し直すのならば、いっそのこと、全部の関数を「完全展開可能」にしてしまおう! そうすればもう「ジレンマ」等に心を悩ませる事態から完全に解放されることになり、さらには、これにより「完全展開可能なナベアツ」の実装を手に入れることができる!((2012-01-19 の記事\NabeAzzX は「制限付展開可能」(普通の TeX の用語だとこれが「完全展開可能」と呼ばれる)でしかない。)) 気分爽快なること間違いなしである! (※よい子のみんなは絶対にマネしないでね!)

(expl3 の意味で)完全展開可能なナベアツ

というわけで、以下のようなことをやってみた。

  • iota と map を用いたロジックを実装する。
  • \zrxxnz_iota:n を完全展開可能な実装に換える。
    →実際には引数の指定方法を \prg_stepwise_〜 (の最初の 3 つ)と同じにして、\zrxxnz_iota:nnn とした。
  • \tl_map_function:nN も完全展開可能なものに置き換える。
    →よく考えると、\tl_map_function:nN は各反復での値をグループで囲った形で出力するので、\NabeAzzX の規定する形式に合わない。だから囲わない出力を行う(完全展開可能な)関数 \zrxxnz_map_flatten:nN を実装した。
  • その他、必要な修正を行って、ナベアツの関数自体を(expl3 の意味で)完全展開可能にする。
  • expl3 で引数の完全展開を表す指定子は「f」なので、関数の名前は \NabeAzzF とする。((「制限付展開可能」は「網羅展開で正しい結果が得られる」ということで、網羅展開を表す指定子は「x」である。といっても実は、\NabeAzzX の X は単に expandable から付けたもので、expl3 の x から来たものではなかった。))
\documentclass{article}
\usepackage{type1cm}
\usepackage{xparse,l3str}
\ExplSyntaxOn %-------------------------

%% 標準で未定義の \exp_args:〜 を作成する
\cs_set_nopar:Npn \exp_args:Nof { \::o \::f \::: }
\cs_set_nopar:Npn \exp_args:Nfff { \::f \::f \::f \::: }

%% \zrxxnz_iota:nnn {<始値>} {<増分>} {<終値>}
% 数列を表すアイテム列を返す. 完全展開可能.
% 例: \zrxxnz_iota:nnn {1} {1} {3} → {1}{2}{3}
\cs_new:Nn \zrxxnz_iota:nnn {
    % 正の増分のみをサポートする(手抜き)
    \int_compare:nNnT {#2} > { 0 } {
        \exp_args:Nf \tl_reverse:n {
            % 引数の整数式は予め評価しておく
            \exp_args:Nfff \zrxxnz_iota_aux:nnnn
              { \int_eval:n {#1} }
              { \int_eval:n {#2} }
              { \int_eval:n {#3} }
              {}
        }
    }
}
  % 第 4 引数のトークン列に結果を(逆順に)累積させていく
\cs_new:Nn \zrxxnz_iota_aux:nnnn {
    \int_compare:nNnTF {#1} > {#3} { % 終了条件
      #4
    } {%else
      % ここでも第 1 引数は先に評価する必要あり
      \exp_args:Nf \zrxxnz_iota_aux:nnnn
        { \int_eval:n { #1 + #2 } } { #2 } { #3 }
        { {#1} #4 }
    }
}

%% \zrxxnz_map_flatten:nN {<アイテム列>} \関数
% アイテム列中の各アイテムに関数を適用した評価値のトークン列を
% 順に(グループで囲まずに)並べたものを返す. 完全展開可能.
\cs_new:Nn \zrxxnz_map_flatten:nN {
    \exp_args:Nnf \zrxxnz_map_flatten_aux_i:nnN
      {} { \tl_reverse:n { #1 } } #2
}
\cs_new:Nn \zrxxnz_map_flatten_aux_i:nnN {
    \tl_if_empty:nTF { #2 } { % 終了条件
        #1
    } {%else
        \exp_args:Nof \zrxxnz_map_flatten_aux_ii:nnNn
          % headのトークン列がそれ以上展開されるのを防ぐため
          % o-展開にして \tl_head:w を使う
          { \tl_head:w #2 \q_stop }
          % こちらは f-展開にする
          { \zrxxnz_tail:n {#2} }
          #3 {#1}
    }
}
\cs_new:Nn \zrxxnz_map_flatten_aux_ii:nnNn {
    \exp_args:Nf \zrxxnz_map_flatten_aux_i:nnN
      { % この引数は完全展開される
        #3 {#1} % 実質的にこの部分の展開となる
        #4 
      }
      {#2} #3
}

%% \zrxxnz_tail:n {<アイテム列>}
% アイテム列の先頭を取り除いたもの. 完全展開可能.
% \tl_tail:n と異なり結果のアイテムが 1 個になっても
% 括弧が外れたりしない.
% 例: \zrxxnz_tail:n { {aa} {Bb} } → {bb}
\cs_new:Nn \zrxxnz_tail:n {
    \exp_after:wN \exp_after:wN \exp_after:wN \use:n
      \exp_after:wN { \use_none:n #1 }
}

%% \zrxxnz_process_step:n {<整数>}
% 最初の版のものと同じ仕様だが, 完全展開可能にしている.
% 整数は数字列で与えられると仮定する.
\cs_new:Nn \zrxxnz_process_step:n {
    % \use:〜 は単に引数に与えられたものを順に出力するだけ
    % の関数. これと \exp_args:〜 の組み合わせで, 特定の
    % 部分だけを展開させられる.
    \exp_args:Nno \use:nn {
        \bool_if:nTF { \zrxxnz_int_is_aho_p:n {#1} } {
            {
                \AhoFont
                #1 % 引数は既に数字列である
            }
        } {
            #1
        }
    } {
        % これは展開されて, "カテゴリコード 10, 文字コード
        % 32 の文字トークン" となる.
        \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 }
    }
}

%% \zrxxnz_x_main:n {<整数>}
% \NabeAzzX の実体.
\cs_new:Nn \zrxxnz_NabeAzzF:n {
    \exp_args:Nf \zrxxnz_map_flatten:nN
      { \zrxxnz_iota:nnn { 1 } { 1 } { #1 } }
      \zrxxnz_process_step:n
}
\cs_new_eq:NN \NabeAzzF \zrxxnz_NabeAzzF:n


%%%% 以下はテスト用 %%%%

%%<*> \testNabeAzzF
\DeclareDocumentCommand \testNabeAzzF {} {
    \exp_args:Nf \tl_show:n { \NabeAzzF { 40 } }
}


%%<*> \AhoFont
\NewDocumentCommand \AhoFont {} {
    \usefont { OT1 } { cmfr } { m } { it } \LARGE
}

\ExplSyntaxOff %------------------------
\begin{document}

%% テスト実行
\testNabeAzzF
% もう組版しなくていいよね ;-)
%\NabeAzzF{40}

\end{document}

テストでは、「\NabeAzzF{40}」を完全展開(exp_args:Nf で展開)した結果を tl_show:n で表示させている。実際の実行結果は以下の通り。

> 1 2 {\AhoFont 3} 4 5 {\AhoFont 6} 7 8 {\AhoFont 9} 10 11 {\AhoFont 12} {\AhoF
ont 13} 14 {\AhoFont 15} 16 17 {\AhoFont 18} 19 20 {\AhoFont 21} 22 {\AhoFont 2
3} {\AhoFont 24} 25 26 {\AhoFont 27} 28 29 {\AhoFont 30} {\AhoFont 31} {\AhoFon
t 32} {\AhoFont 33} {\AhoFont 34} {\AhoFont 35} {\AhoFont 36} {\AhoFont 37} {\A
hoFont 38} {\AhoFont 39} 40 .

実装のポイント

関数を完全展開可能にするには、途中の展開形において、先頭のトークンを常に関数(マクロ)にしておく必要がある。(つまり「最後の展開」でようやく先頭が関数でなくなって、それは「正しい結果」になっているということ。)これを実現するために、私は「関数型言語における末尾再帰型になるリスト処理の実装方式」を利用した。もちろんここでの要求は末尾再帰ではないのだが、「反復中の式の形が、ある関数の適用のままずっと保たれる」という性質は今述べた要求を満たすのに十分である。

当然であるが、展開可能な関数を実装しようとすると、展開制御(\exp_args:〜)を必要とする場面が多くなる。上掲のプログラムでは、標準で用意されている以外の組み合わせが必要となったので、expl3 のマニュアルの説明に従って自分で定義している。

あと \zrxxnz_tail:n というのが気になるかも知れないが、これは次回に説明する。

(まだ続けるつもりですか……)