マクロツイーター

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

LISP on TeX で普通に組版してみる

アレの新版がリリースされた

……誰もやらないね(´・ω・` )

仕方がないので

そういう訳で、ネタを作ることにした。

TeX 上で「普通の言語」を動かす、というとどうしても「TeX 言語では不可能あるいは困難な、何か非常に複雑な処理をする」(例えば「マンデルブロー集合する」等)ことに心が奪われがちである。しかし、「普通の言語が動く」ことによる利は何も TeX 芸人だけが享受すべきものではない。寧ろ、一般の LaTeX ユーザ(ただし「普通のプログラム言語」が使える者)こそがその恩恵を受けるべきであろう。そういうわけで、「文書作成中に遭遇するちょっと複雑な処理」を LISP プログラムで解決する、というネタをやってみよう。

構造的データから表組みを行う

LaTeX の表組の記述はよく煩雑であると批判されている。実際には、表組そのものを表すためのメタ記述は(HTML 等と比較すると)かなり単純であろう。ところが、凝った見栄えの表を作ろうとしてレイアウト設定を増やしていくと、どうしても票の中身の記述がレイアウトの記述に紛れてしまって、結果、判別が非常に困難になってしまうことが多いのは確かである。

この類の問題を解決する手段が「レイアウトの記述とデータの記述を分離すること」である。一般的な話としては、LaTeX ではマクロ(ユーザ定義命令)を活用してマークアップを自分で定義することである程度「分離」が実現できることが多い。ところが、表組(tabular/array 環境)に関しては、内部での TeX の処理がやや特殊であるため、「環境の中に自前のマークアップ(ユーザ定義命令)を持ち込む」ことが基本的にできない。(TeX 言語のマクロでは一応実現可能ではあるが、それでも割と高度な技術が要求される。)

ところが、LISP on TeX を用いると次のようにして「分離」が実現できる。

  • 「データ」を LISP 上の値(S 式)として表す。
  • データの値を受け取って「表組みの LaTeX ソースの文字列」を返す LISP の関数(\xShowTable)を実装する。(これが「レイアウトの記述」に相当する。)
  • \lispinterp{(\texprint (\xShowTable〈データ〉))}」を実行する LaTeX のマクロを作る。

この方式だと、tabular 環境の「完成したソースコード」が一括して作られて実行されることになるので、先に述べた「環境内でマクロが使えない」問題を回避することができるのである。

実装した例を挙げる。なお、元データはとある TeX なブログに掲載されているものを用いた。*1

\documentclass[a4paper]{jsarticle}
\usepackage{lisp-on-tex}
\newcommand\showtable[1]{\lispinterp{(\xShowTable #1)}}
\lispinterp{
(\define \xShowTable (\lambda (\dat)
  (\texprint (\xForm \dat))))
%% メンバ抽出
(\define \xHdX (\lambda (\d) (\nth \d :0)))
(\define \xHdC (\lambda (\d) (\nth \d :1)))
(\define \xHdS (\lambda (\d) (\nth \d :2)))
(\define \xRows (\lambda (\d) (\nth \d :3)))
(\define \xHdR (\lambda (\r) (\nth \r :0)))
(\define \xRow (\lambda (\r) (\nth \r :1)))
%% 書式化
(\define \xForm (\lambda (\d) (\concat
  '\begin{tabular}' (\group (\xCols \d))
  '\hline\hline'
  (\xHdX \d) '&' (\xJoin '&' (\xFormHdC (\xHdC \d))) '&' (\xHdS \d)
  '\\\hline'
  (\xJoin '\\' (\map (\lambda (\r) (\concat
    (\xHdR \r) '&' (\xJoin '&' (\xFormHdB (\xRow \r)))
    )) (\xRows \d)))
  '\\\hline\hline'
  '\end{tabular}'
)))
(\define \xCols (\lambda (\d) (\concat
  'c' '*' (\group (\intTOstring (\length (\xHdC \d)))) '{rr}r')))
(\define \xFormHdC (\lambda (\cc) (\map (\lambda (\c)
  (\concat '\multicolumn{2}{p{6zw}}' (\group (\concat '\centering' \c)))
  ) \cc)))
(\define \xFormHdB (\lambda (\cc)
  (\let ((\sum (\xFold \+ :0 \cc)))
    (\xFold \xAppend () (\xAppend
      (\map (\lambda (\c) (\list (\intTOstring \c) (\xRatio \c \sum))) \cc)
      (\list (\list (\intTOstring \sum))))))))
(\define \xRatio (\lambda (\x \y)
  (\concat '(' (\intTOstring (\/ (\+ \y (\* \x :200)) (\* :2 \y))) '\%)')))
% 補助関数
(\define \xAppend (\lambda (\la \lb) (\xFold \cons \lb \la)))
(\define \xJoin (\lambda (\s \lst)
  (\lispif (\= () \lst) ()
    (\let ((\l (\xxFold \cons \lst ())))
      (\xxFold (\lambda (\x \y) (\concat \x \s \y)) (\cdr \l) (\car \l))))))
(\define \xFold (\lambda (\fun \i \lst)
  (\xxFold \fun (\xxFold \cons \lst ()) \i)))
(\define \xxFold (\lambda (\fun \l \r)
  (\lispif (\= () \l) \r (\xxFold \fun (\cdr \l) (\fun (\car \l) \r)))))
}
%% デバッグ用
\newcommand\TEST[1]{\lispinterp{(\texprint (\concat '\TESTa' (\group #1)))}}
\newcommand\TESTa[1]{\texttt{\detokenize{#1}}}
\begin{document}
\begin{table}[!h]
  \centering
  \caption{東大化学の設問数推移}
  \showtable{(\quote
    %% データ
    ( '年度' ('第1問\\(理論)' '第2問\\(無機)' '第3問\\(有機)') '合計' (
    ( '1987年' (   :7   :7   :9 ) )
    ( '1988年' (   :7   :4   :8 ) )
    ( '1989年' (   :3   :6  :11 ) )
    ( '1990年' (   :4   :8  :13 ) )
    ( '1991年' (   :7   :8   :9 ) )
    ( '1992年' (   :3   :9  :11 ) )
    ( '1993年' (   :3   :6   :5 ) )
    ( '1994年' (   :4   :4   :8 ) )
    ( '1995年' (   :5   :6   :8 ) )
    ( '1996年' (   :4   :5   :6 ) )
    ( '1997年' (   :5   :7   :8 ) )
    ( '1998年' (   :5   :7   :9 ) )
    ( '1999年' (   :5   :9   :9 ) )
    ( '2000年' (   :5   :8  :10 ) )
    ( '2001年' (   :5   :9   :9 ) )
    ( '2002年' (   :6   :9   :8 ) )
    ( '2003年' (   :7   :7  :11 ) )
    ( '2004年' (   :7  :11  :12 ) )
    ( '2005年' (  :10   :9   :8 ) )
    ( '2006年' (   :6  :10   :7 ) )
    ( '2007年' (   :8  :12   :7 ) )
    ( '2008年' (   :8   :9  :10 ) )
    ( '2009年' (  :10  :10   :9 ) )
    ( '2010年' (   :8   :9   :9 ) )
    ( '2011年' (  :11  :11  :10 ) )
    ( '2012年' (   :9  :11  :11 ) )
    ( '2013年' (  :11  :12  :13 ) )
    ( '2014年' (  :11  :14  :15 ) )
    ))
  )}
\end{table}
\end{document}

上の例そのものに関しては、それほど「凝ったレイアウト」を施している訳でもないので、「分離」のメリットがそれほど得られるわけではない。しかし、この方法を応用することで、これまで難しかった「分離」ができることは注目すべきであろう。

まとめ

そういうわけで。

LISP on TeX は実用的な技術である。

何だか極めて強引な論理展開のように見えるが、気にしないことにしよう。

*1:「設問数の割合を出す」ことに何か意味があるか、というと無論何もない。単に「ちょっと複雑な加工処理」の例として行っている。