TeX でリスト処理

日々TeX言語でプログラムを書いているLisperなら一度は「ここでLispみたいにリスト処理ができたらなぁ」と
感じたことがあるだろう. *1 大抵の場合, 要求される演算はcdrとかlengthとか
for-eachの様な簡単な処理なので, コンマ区切りのリストを使ってその場を凌いだりする...のだろうか.
(TeXで配列が使えることを自分が知ったのは相当後だったと思う.) しかしコンマ区切りのリストでは
それ以上の標準的なリスト処理を即興で書くのは面倒くさい. LISP on TeX
(https://bitbucket.org/hak7a3/lisp-on-tex)の様な高機能なものもあるけど,
(そもそもLoTのことをよく知らないし,) あくまでTeXからちょろちょろっとリストを使いたいだけにしては
大仰だ.


代数的データ型としての実装

ZRさんち id:zrbabbler:20120115:1326571496 ではリストを代数的データ型として扱っていた.
*2 こんな風に:

% LIST := \mwml@NULL | \mwml@CONS{ELEM}{LIST}
% ELEM := anything
\def\mwml@CONS#1#2{}
\def\mwml@NULL{}

% リストの例
\def\listA{\mwml@CONS{A}{\mwml@CONS{B}{\mwml@NULL}}}

さて, リストに新しい要素を付け加えリストを作る \mwml@set@cons を作ろう. これは #1 に制御綴りを取り,
#2 と #3 を cons したリストを束縛する. ZRさんの記事では CONS と NULL を凍結した状態で \edef
するという方法だったが, これでは頑強でないマクロを要素として持つリストを扱えない.
これは \mwml@CONS を \relax に束縛した状態で 第三引数を \expandafter することで解決できる.

\def\mwml@with@freezed@list#1{%
  \let\mwml@CONS@before\mwml@CONS
  \let\mwml@NULL@before\mwml@NULL
  \let\mwml@CONS\relax% 一旦凍結して
  \let\mwml@NULL\relax
  #1% 実行する
  \let\mwml@CONS\mwml@CONS@before
  \let\mwml@NULL\mwml@NULL@before
}

\def\mwml@set@cons#1#2#3{%
% #2 と #3 のconsを #1 に束縛する
  \mwm@in@group@with@nonlocal#1#1{% \begingroup ... \endgorup だが #1 を \endgroup の外に持ち出す
    \mwml@with@freezed@list{%
      \toks2{#2}%
      % #3 が制御綴のときはそれが(一段階だけ)展開される. そうでないときは
      % (リスト #3 は \mwml@CONS か \mwml@CONS で始まるはずで, それは今 \relax してるから)何もしない.
      \toks3\expandafter{#3}%
      \edef#1{\mwml@CONS{\the\toks2}{\the\toks3}}%
    }%
  }%
}

\def\mwm@in@group@with@nonlocal#1#2#3{%
  \begingroup
  #3%
  \global\let\mwm@in@group@with@nonlocal@@a#1% 一旦大域変数に入れて
  \endgroup
  \let#2\mwm@in@group@with@nonlocal@@a% それを戻す
}

第三引数には制御綴りでも生のリストでもどちらでも渡すことができる. 第二引数は展開されないことに注意.

% (setq listA '(A B))
% (setq listB (cons 'Z listA))
\def\listA{\mwml@CONS{A}{\mwml@CONS{B}{\mwml@NULL}}}
\mwml@set@cons\listB{Z}\listA
% => \mwml@CONS{Z}{\mwml@CONS{A}{\mwml@CONS{B}{{\mwml@NULL}}}}

% (setq listC (cons {\nonrobustcmd ごにょごにょ} '(\nonrobustcmd)))
\mwml@set@cons\listC{\nonrobustcmd ごにょごにょ}{\mwml@CONS{\nonrobustcmd}{\mwml@NULL}}
% => \mwml@CONS{\nonrobustcmd ごにょごにょ}{\mwml@CONS{\nonrobustcmd}{\mwml@NULL}}

car と cdr はZRさんちのものをほぼそのまま.

\def\mwml@set@car#1#2{%
% Bind car of #2 to #1
  \mwm@in@group@with@nonlocal#1#1{%
    \def\mwml@CONS##1##2{\def#1{##1}}%
    \let\mwml@NULL\undefined
    #2%
  }%
}
\def\mwml@set@cdr#1#2{%
% Bind cdr of #2 to #1
  \mwm@in@group@with@nonlocal#1#1{%
    \def\mwml@CONS##1##2{\def#1{##2}}%
    \let\mwml@NULL\undefined
    #2%
  }%
}

ここまで来れば後は欲しい函数をどんどん実装するだけである.


例題: 行列を生成する

数学をやっていると行列を書く機会が多い. しかし対角成分のみの行列を書くのに毎回 pmatrix 環境を
書くのは面倒だ. 対角成分が A, B, \ddots, D で右上が*, 左下が空の行列なら

\begin{pmatrix}
  A & * & \cdots & * \\
    & B & \cdots & * \\
    &   & \ddots & \vdots \\
    &   &        & D
\end{pmatrix}

ではなく単純に

\[
  \diagmatrix[*,] A, B, ..., D ;
\]

と書きたい. *3

Lispの演習問題としては簡単だ. Scheme(Gauche)で書くなら, こんな感じになるだろう.
(TeXに移植するのでLispとしては少し不自然. というかTeXのコードを逆にLispに直した)

(use srfi-1)
(use srfi-11)
(use srfi-13)

(define (diagmatrix ufill lfill diagonals)
  (define (dots? str)
    (rxmatch #/^ *(\.\.\.|\\dots)$/ str))
  (define (mask-with e b replacement)
    (if b
        replacement
        e))
  (define (make-row d i b right-cdots left-cdots mask right-line left-line right-dots-line left-dots-line)
    (let-values (((left-mask r) (split-at mask i)))
      (let1 right-mask (cdr r)
        (if b
            `(,@(map (cut mask-with <> <> "") left-dots-line left-mask)
              "\\ddots"
              ,@(map (cut mask-with <> <> "") right-dots-line right-mask))
            `(,@(map (cut mask-with <> <> left-cdots) left-line left-mask)
              ,d
              ,@(map (cut mask-with <> <> right-cdots) right-line right-mask))))))
  (let* ((r (if (string-null? ufill)
                '("" . "")
                '("\\cdots" . "\\vdots")))
         (right-cdots (car r))
         (right-vdots (cdr r))
         (l (if (string-null? lfill)
                '("" . "")
                '("\\cdots" . "\\vdots")))
         (left-cdots (car l))
         (left-vdots (cdr l))
         (mask (map dots? diagonals))
         (size (length diagonals))
         (indice (iota size 0 1))
         (right-line (make-list size ufill))
         (left-line (make-list size lfill))
         (right-dots-line (make-list size right-vdots))
         (left-dots-line (make-list size left-vdots))
         ; row は実際には行列が入る
         (row (map (cut make-row <> <> <>
                        right-cdots left-cdots mask right-line left-line right-dots-line left-dots-line)
                   diagonals indice mask)))
    row))

試しに実行してみよう.

(diagmatrix "*" "0" '("A" "B" "..." "D"))
; => (("A" #0="*" #1="\\cdots" #0#)
;     (#2="0" "B" #1# #0#)
;     (#3="\\vdots" #3# "\\ddots" "\\vdots")
;     (#2# #2# "\\cdots" "D"))

; 見難いが, (2,4)-成分と(4,2)-成分が空になっている
(format "~s" (diagmatrix "0" "0" '("a_1" "..." "a_i" "..." "a_n")))
; => "((a_1 \\cdots 0 \\cdots 0)\
;     (\\vdots \\ddots \\vdots  \\vdots)\
;     (0 \\cdots a_i \\cdots 0)\
;     (\\vdots  \\vdots \\ddots \\vdots)\
;     (0 \\cdots 0 \\cdots a_n))"
(diagmatrix "0" "0" '("a_1" "..." "a_i" "..." "a_n"))
; => (("a_1" #0="\\cdots" #1="0" #0# #1#)
;     (#2="\\vdots" #3="\\ddots" #4="\\vdots" "" #4#)
;     (#5="0" #6="\\cdots" "a_i" #0# #1#)
;     (#2# "" #2# #3# #4#) (#5# #6# #5# #6# "a_n"))

mwmlist.styを使えばこのコードをほぼそのまま移植できる. mainの部分だけなら30行しかない.
(わかりにくいが, 頑強でない要素を取れるようになったので実装自体も少し簡単になっている.
頑強なものを扱えない場合は最初の部分で \mwm@dm@@right@cdots 等を \relax にしておき, 最後に
pmatrix 環境の中身を生成した後で適切なdotのマクロを束縛しなければならない.)

% []の部分を読み取って \mwm@dm@main を呼び出す
\def\diagmatrix{% [ufill,lfill] CSL ;
% Diagonal matrix whose diagonal elements are CSL, upper triangle filled with ufill,
% lower with lfill. In CSL, the strings "..." and "\dots" following spaces have special meaning.
% Matrix elements of rows and columns <dots> exist are replaced with appropriate dots
% as the element see how many <dots> in the diagonal. For example, if an element sees
% <dots> vertically and no <dots> horizontally, it is replaced with \cdots .
  \begingroup
%  \def\mwm@dm@@upper@filling{}%
%  \def\mwm@dm@@lower@filling{}%
  \@ifnextchar[ \mwm@dm@bite@bracket {\mwm@dm@bite@bracket[,]}%
}
    \def\mwm@dm@bite@bracket[#1]{%
      \mwm@dm@bite@bracket@aux#1,\@nil
      \mwm@dm@main
    }
        \def\mwm@dm@bite@bracket@aux#1,#2\@nil{%
          \def\@tempa{#2}%
          \ifx\@tempa\@empty
            \def\mwm@dm@@upper@filling{#1}%
            \def\mwm@dm@@lower@filling{#1}%
          \else
            \mwm@dm@bite@bracket@aux@two#1,#2\@nil
          \fi
        }
            \def\mwm@dm@bite@bracket@aux@two#1,#2,\@nil{%
              \def\mwm@dm@@upper@filling{#1}%
              \def\mwm@dm@@lower@filling{#2}%
            }
    \def\mwm@dm@main#1;{%
      % 色々設定する
      % filling が空ならドットはなし
      \let\mwm@dm@@ddots\ddots
      \ifx\mwm@dm@@upper@filling\@empty
        \let\mwm@dm@@right@cdots\relax
        \let\mwm@dm@@right@vdots\relax
      \else
        \let\mwm@dm@@right@cdots\cdots
        \let\mwm@dm@@right@vdots\vdots
      \fi
      \ifx\mwm@dm@@lower@filling\@empty
        \let\mwm@dm@@left@cdots\relax
        \let\mwm@dm@@left@vdots\relax
      \else
        \let\mwm@dm@@left@cdots\cdots
        \let\mwm@dm@@left@vdots\vdots
      \fi
      \mwml@set@list@from@csl\mwm@dm@@diagonals{#1}% コンマ区切りリストからリストへ変換
      \mwm@dm@make@mask% どの対角成分に「ドット」が入っているかのマスクを作る. \mwm@dm@@mask に束縛する.
      \mwml@set@length\mwm@dm@@size\mwm@dm@@diagonals
      \mwml@set@iota\mwm@dm@@iota\mwm@dm@@size{0}{1}% 普通の行列の添字とは異なることに注意.
      \mwml@set@make@list\mwm@dm@@right@line\mwm@dm@@size\mwm@dm@@upper@filling% ufill を並べたリスト
      \mwml@set@make@list\mwm@dm@@left@line\mwm@dm@@size\mwm@dm@@lower@filling% lfill を並べたリスト
      \mwml@set@make@list\mwm@dm@@dots@left@line\mwm@dm@@size{\mwm@dm@@left@vdots}%% 左側のドットを並べたリスト
      \mwml@set@make@list\mwm@dm@@dots@right@line\mwm@dm@@size{\mwm@dm@@right@vdots}%% 同様
      % メインの処理
      % \mwm@dm@make@row (3引数函数) は返り値を \mwm@dm@@row に入れる.
      % #1 は #2 番目の対角成分, #3 は #1 が「ドット」かどうか. 返り値は #2 番目の行を表すリスト.
      \mwml@set@map@three\mwm@dm@@row\mwm@dm@make@row\mwm@dm@@diagonals\mwm@dm@@iota\mwm@dm@@mask
      % \mwml@set@map@three は生成したリストを \mwm@dm@@row に入れる.
      \mwml@output@matrix@with@pmatrix\mwm@dm@@row% 行列を表示する
      \endgroup
    }
% 続く...

あとは残った補助函数を書けばいいのだが, (コメントを書くのが)面倒なので興味があれば
https://gist.github.com/kenoss/6384624を見てみるとよい. (\diagmatrixは全部で120行.)

*1:もちろん個人差がある.

*2:書いてから思ったけど, この記事パクリ以外の何ものでもないのでは...

*3:実のところ, 普通に配列でやろうとして挫折して mwmlist.sty を書いた.