2014/11/10
TeXユーザの集い2014でしたTUG2014の話
2013/12/31
2014年賀状的なもの
今年は喪中だから年賀状じゃないんだからね。
%! << /PageSize [285 420] >> setpagedevice /setrandcolor { /m exch .1 add def /r1 {rand 5 mod 1 div m add} def /r2 {rand 3 mod 3 div m add} def /r3 {rand 3 mod 3 div m add} def r1 r2 r3 setrgbcolor } def /f {% def dup dup dup 4 mod 0 eq {(2) 4 1 roll -1 0 rmoveto} if 4 mod 1 eq {(0) 3 1 roll -3 0 rmoveto} if 4 mod 2 eq {(1) 2 1 roll -3 0 rmoveto} if 4 mod 3 eq {(4 ) -5 0 rmoveto} if } def /drawyear { % def 4 1 64 { % for /i exch def /v { i i mul i mul 0.0003 mul } def /w { i 15 add 2 div } def /d { -360 w mul 2 3.1415 mul 65 mul div } def % -360w/(2pi*65) d rotate /Titania findfont 2 w mul scalefont setfont v 0 rmoveto 0.001 setrandcolor i f show } for } def 1 1 20 { % for /n exch def 93 160 translate 135 rotate 0 0 moveto drawyear 100 rotate 150 -140 moveto 0 setgray /Palatino-Bold-Italic findfont 12 scalefont setfont n =string cvs show 0 -2 rmoveto (/) show 0 -2 rmoveto (20) show showpage } for
例年に増して芸がない感じもするけどシンプルなのがいちばんということで。
2013/12/20
Learn You The TeX for Great Good!
「第3章 マクロ初級講座」
[前回までのあらすじ]
LaTeX の原稿に記されている文字を別の文字に置き換えて出力したくて TeX マクロを書いてみたけど、なぜかうまく動かない。 TeX はチューリング完全なプログラミング言語だって聞いたけど、こんな基本的なテキスト編集の機能さえ持ち合わせてないのでは話にならない。 TeX も LaTeX も滅ぶべき。
Note:この記事は、TeX & LaTeX Advent Calendar 2013 の20日目の記事です。昨日は zr さんによる「テキストの装飾の指定: CSSとLaTeXの比較」です。明日の21日目は shigeyas さんの予定です。
LaTeX のことはきっぱり忘れてください(「TeX」といったら1982年に書き直されたいわゆる TeX82 と、その直系にあたるバージョン3のバグフィックス版たちのことです)。 ここでは TeX で文字の置換に「近い」ことをしてみましょう。 再帰的な TeX マクロをどうやって書くか、何となく感じてもらうのが本章のゴールです。
再帰!帰!!
いま、与えられた文字列中のすべての文字を「*
」に変換して伏字化した結果が欲しいとします。「brainfuck
」→「*********
」という具合です。パスワードの入力欄みたいな雰囲気ですね。
ぱっと思いつくのは、こんな再帰的なマクロを使った例です。これは実際、上記の例「brainfuck
」→「*********
」ならうまく変換してくれます。
\def\ast#1{\recur#1!} \def\recur#1{% \ifx!#1\else*\expandafter\recur\fi}
\ast{brainfuck}
とすると TeX の中で何がおきるのか、順番に見ていきましょう。
\ast
マクロは、引数の末尾に !
をつけてから、ブレース {}
を取っ払った状態で \recur
に引き渡します。
「 \recur brainfuck!
」という形で \recur
マクロに引数が渡されるということです。
この状態で \recur
マクロが引数だと思うのは、 brainfuck!
という文字列ではなく、先頭の b
だけです。
ナンデと思うかもしれませんが、ブレースがない状態でマクロに何かを渡すと、その先頭だけが引数としてマクロに渡されるのです。
これは、TeX において \def
を使って定義できるのは、一般的なプログラミング言語における関数とかサブルーチンとかクロージャーとかではなく、「置き換えルール」だからです。
置き換えルールを \def
で定義する際には、置き換えの対象となるパターンを指定するときのプレースホルダとして、 #1
のようなパラメータ引数が使えます。
たとえば \def\recur#1{foo #1 bar}
によって「定義」されるのは、「\recur
の直後に「何か」が一個あったら、それを #1
で示されるパラメータ引数に束縛して、 foo
と bar
で囲む」という置き換えルールです。
じゃあ、なぜ最初に \ast{brainfuck}
としたときには、文字列 brainfuck
が #1
に束縛されたのでしょうか?
それは、ブレースで囲まれて渡されたパラメータ引数は、そのブレースを取り除いた状態でまるごと束縛されることになってるからです。
トークンとトークンリスト
上記では「何か」とぼかしましたが、置き換えルールが適用される対象は「トークン」と呼ばれています。 トークンとは具体的には何なのでしょうか?
簡単にいうと TeX のトークンとは、「文字(カテゴリコードという TeX 特有の属性つき)」または「コントロールシーケンス(いわゆる \
ではじまるやつ)」です。
TeXは、読み込んだファイルに含まれる文字や数字や記号を、まずこの「トークン」の列へと咀嚼します。
そうして得られたトークンの列は、トークンリストと呼ばれます。
Note:文字トークンとコントロールシーケンス以外に、マクロ定義に出てくる #1
みたいなパラメータ引数もトークンとみなされてるのですが、忘れてかまいません
このトークンリストには、原稿を書いた人が入力したコントロールシーケンスなんかもみんな含まれているわけですが、それを最終的に出力するデータ用のトークンリストへと展開するのがTeXの次の仕事です。
ここまでをまとめると、 \ast{brainfuck}
は \ast
の定義によって \recur brainfuck!
に置き換えられ、 \recur brainfuck!
は \recur
の定義によって \ifx!b\else*\expandafter\recur\fi rainfuck!
に置き換えられます。
b
だけが \recur
に渡された結果、残りの rainfuck!
が後ろにくっついているのに注意してください。
\ifx は直後のトークンを比較して展開する場所を制御
\ifx
は、直後の2つのトークンが「同じ」なら \else
または \fi
までを読み込み、違ったら読み飛ばすという TeX の命令です。
さきほど \ast
マクロを利用したときに、ブレースを使って引数を {foo}
のように囲んでグループとして渡せるといいました。
ブレースで囲んだ「何か」は、トークンではありません。
グループを開始する左ブレースというトークン、ブレースの間にあるトークンたち、それにグループを終了する右ブレースというトークンからなる、トークンリストの一部です。
したがって、たとえば \ifx {foo}\x
のようにしても、 x
が文字列 foo
か調べて条件分岐することにはなりません。
もし \ifx {foo}\x
と書いたら、 {
と f
が「同じ」かどうか調べた挙句、当然 {
と f
は「同じ」じゃないので、 \else
または \fi
まで読み飛ばされて意図しない結果になります。
Note:\ifx
は直後の2つのトークンをとって、それらを展開せずに、同じかどうかを調べます。 もしマクロ定義の中で\ifx #1!
のように書いて(#1
と!
が逆になってることに注目)、展開時に#1
にbrainfuck
が束縛されたとしたら、 先頭の2つのトークンb
とr
が比較されて常に\else
または\fi
まで読み飛ばされてしまいますよ。
\ast{brainfuck}
の例に戻りましょう。
!
と b
は「同じ」じゃないので、次の条件分岐は、
\ifx!b\else*\expandafter\recur\fi rainfuck!
else節のようなものに処理がわたってこんなふうな結果になるように思えるかもしれません。
*\expandafter\recur rainfuck!
しかし、実際にはこうなります。
*\recur rainfuck!
\expandafter
はいったいどこにいってしまったんでしょう?
実は \ifx
のような条件分岐命令は、TeX が「展開」するトークンを条件に基づいて制御するものであり、「条件を満たさなかったら \else
から \fi
までの部分を実行する」ような代物ではありません。
この場合は、「条件を満たさなかったら \else
までを読み飛ばす」だけです。
なので \ifx!b
まで展開した TeX は、条件を満たさないので次の \else
まで読み飛ばし、 *\expandafter\recur\fi rainfuck!
の展開を試みることになります。
展開順の制御は \expandafter から
いまはトークンリストを展開しているので、最初に出てくる *
は単なる文字ではなく、カテゴリコードという値がついたトークンです。
最終的にはアスタリスクが出力されるし、単なる文字との違いを意識しなければならないわけではないのですが、トークンリストに読み込まれているのは、あくまでも記号などの文字を意味するカテゴリコード12がついた文字トークンです。
その次にくるのが、\ifx
によって消えてしまったかのように見える \expandafter
です。これは「直後のトークンの展開を待たせる」という仕事をする TeX のプリミティブです。
ここに \expandafter
があるため、 TeX はトークンリストで次にくる \recur
のことは忘れて、 \fi
を展開します。これは言うまでもなく、条件分岐が一つ終わりますという命令です。
なので、先に \ifx
から始まった条件分岐は、この時点で無事に終わります。
ここで、先ほど \expandafter
されていた \recur
が元の位置に戻されます。
その後の rainfuck!
は、全部文字トークンです。
結果として得られるトークンリストが *\recur rainfuck!
というわけです。
というわけで、 \recur brainfuck!
が *\recur rainfuck!
になりました。
ここで再び \recur
が展開されて **\recur ainfuck!
になり、さらに \recur
が展開されて ***\recur infuck!
になり、…… *********\recur !
まで同様に展開されていきます。
最後の !
は、 \recur
に読まれると \ifx !!
となり条件を満たすため、今度は \fi
まで一気に読み飛ばされます。
その結果得られるトークンは何もないので、最終的に一連の文字トークン *********
が TeX の出力処理に回ることになります。
これが TeX プログラミングにおける末尾再帰です。まるでパズルですね。
さらに深く
この \ast
には重大な制約があります。
まず明らかな制約として、再帰の基底ケースとして !
を使っていることから、 !
が含まれる文字には使えません。
さらに、文字列の中に対応するブレースで囲まれた部分があると、その全体が一つの *
に変換されてしまうという問題があります
( \recur {foo}bar
が何に展開されるかを上記と同じ要領で考えると、なぜだかわかりますよね)。
Note:ブレースで囲んで入力するとトークンリスト上に1つのトークンとしてまとめられるわけではないことに注意してください。このようなブレース区切りは、マクロ定義にあるパラメータ引数(#1
sなど)にトークンをマッチさせるときだけ意味をなします。 たとえば\ifx
の条件チェックでブレース区切りが意味を持たないことは説明しましたが、さらに、たとえば\ifx ... \else ... \fi
の中で左右のブレースが閉じていないようなマクロを書くことも可能です。 この性質は、実用的なマクロの多くでも実際に利用されています。
いちばん大きな制約というより欠陥は、この \ast
マクロはスペースを文字として勘定してくれないことです。
TeX は、スペース(空白)を読み込むとき、暗黙に二通りの解釈をしてトークンリストを作ります。
空白のつもりでファイルに入力したのにトークンリストには何も詰め込まれなかったり、逆に無意識に入れていた何かが空白を表すトークンとしてトークンリストに詰め込まれたりするのです。
空白を意図通りにトークンリストに詰め込むためには、 TeX の文字解釈と展開とマクロのパラメータに対するさらなる理解が無駄に求められます。
そもそも、ここまで「文字列」とぶっちゃけて言ってきましたが、 TeX にそんなデータ型はありません。 ドキュメントを書くのに入力している文字たちは、あくまでもトークンリスト上のトークンを形成するものです。 それを意識しながら TeX ソースを読んだり書いたりすると、 TeX マクロってエンジニアリング的には微妙だけど仕組みとしては意外と面白いかもと感じてきます。 見た目のプログラミング言語っぽさに惑わされると不自由さにがっかりするかもしれませんが、トークンリストの展開順序をコントロールして遊ぶゲームだと思うとなかなか挑戦しがいがある時間泥棒だったりします。
最後に
「!
」の制約はおいといて(ほんとはおいといちゃだめだけど)、代入を使わずにブレースとスペースの問題に対処すると、「TeX 芸人二段」 という称号がもらえることになっています。
ひまなときにチャレンジしてみよう!
この記事は「Learn You A Haskell for Great Good!」というHaskellのチュートリアルのパロディーです。ほかの章はありません。TeXに関する説明には慎重を期したつもりですが、内容には執筆者の勘違いが含まれている可能性があるので、うそを見つけたらぜひコメントをください。
2013/12/18
colortbl で \cline を使いたい
日本でLaTeXが生きのこるには、縦横無尽に罫線が引かれた彩り豊かな表を描けなければならない。
でも、表のセルに色を塗るための colortbl
パッケージと、横線を好きな場所に引くための標準的な方法である \cline
には、罫線までもが色で塗りつぶされてしまうという困った問題があります。
\begin{tabular} {|>{\color{white}\columncolor{red} }c |>{\color{white}\columncolor{green}}c |>{\color{white}\columncolor{blue} }c|} \hline a & b & c \\ \cline{1-1}\cline{3-3} 1 & & 3 \\ \hline \end{tabular}

本当なら、こんな具合に、左右の列だけにセル間の横罫線が引かれてほしいわけです。

これは TeX & LaTeX Advent Calendar の18日目の記事です。 昨日はabenoriさんの「可換図式を書こう:tikz-cdを使う.」です。
公式見解
この \cline
が上書きされてしまう現象は、 colortbl
のマニュアルでも言及されている既知の問題です。
いわく、「これは \cline
の仕様なので代わりに \hhline
を使え」とあります。
10 Less fun with\cline
Lines produced by\cline
are coloured if you use\arrayrulecolor
but you may not notice as they are covered up by any colour pannels in the following row. This is a `feature' of\cline
. If using this package you would probably better using the - rule type in a\hhline
argument, rather than\cline
.
実際、"TeX - LaTeX Stack Exchange" における質問でも、 \hhline
を使った回答が寄せられているようです(参照1、参照2)。
しかしぶっちゃけ \hhline
はまったく解決策になりません。
それどころか、今度は罫線を引きたくない箇所に白い線が出現したり、縦罫線が切断されたりしてしまいます。
\usepackage{hhline} ... \begin{tabular} {|>{\color{white}\columncolor{red} }c |>{\color{white}\columncolor{green}}c |>{\color{white}\columncolor{blue} }c|} \hline a & b & c \\ \hhline{-|~|-} 1 & & 3 \\ \hline \end{tabular}

これはひどい。
ごまかし方
colortbl
で特定の列にだけ横罫線を引くことは不可能なのでしょうか。
実は、方法がないではないので、それを紹介します。ただし後述するように、厳密にいうとこの解法には落とし穴があるので注意してください。
そもそもなんで罫線が塗りつぶされるかというと、 colortbl
では罫線を引くプロセスのあとにセルに色を塗っているからなのですが、これは TeX の仕組み的にはチートできません。
色を塗るにはセルの大きさが決定していなければならず、その段階では罫線も引き終わることになっているからです。
でも逆に考えれば、セルの高さに対して罫線の位置をはじめからずらして引くようにしてしまえば、罫線を微妙に避けて色を塗ってくれるはずです。
そこで次のような \cline
の定義(もとは latex.ltx
にあるけど colortbl
パッケージで上書きされてるもの)を……
%% \@cine from colortbl.sty \def\@cline#1-#2\@nil{% \omit \@multicnt#1% \advance\@multispan\m@ne \ifnum\@multicnt=\@ne\@firstofone{&\omit}\fi \@multicnt#2% \advance\@multicnt-#1% \advance\@multispan\@ne {\CT@arc@\leaders\hrule\@height\arrayrulewidth\hfill}% \cr \noalign{\vskip-\arrayrulewidth}}
次のように書き換えてプリアンブルに書きます(\makeatletter
を忘れずに)。
\def\@cline#1-#2\@nil{% \noalign{\vskip-\arrayrulewidth} \omit \@multicnt#1% \advance\@multispan\m@ne \ifnum\@multicnt=\@ne\@firstofone{&\omit}\fi \@multicnt#2% \advance\@multicnt-#1% \advance\@multispan\@ne {\CT@arc@\leaders\hrule\@height\arrayrulewidth\hfill}% \cr}
元の定義で一番最後にあった \noalign{\vskip-\arrayrulewidth}
を先頭にもってきただけで、ほかはそのままです。
これだけで、ふつうに \cline
を使えば横罫線が出現するようになります。もちろん不要な箇所に白線が出ることもありません。
\begin{tabular} {|>{\color{white}\columncolor{red} }c |>{\color{white}\columncolor{green}}c |>{\color{white}\columncolor{blue} }c|} \hline a & b & c \\ \cline{1-1}\cline{3-3} 1 & & 3 \\ \hline \end{tabular}

やったね。
うまくないところ
この方法は、罫線を、罫線の幅だけ上にちょっとずらすことで、一見成功しているように見える結果を得ています。 なので、ずれてない本来の位置に罫線を一緒に引いてみると、アラがわかります。
\begin{tabular} {|>{\color{white}\columncolor{red} }c |>{\color{white}\columncolor{green}}c |>{\color{white}\columncolor{blue} }c|} \hline a & b & c \\ \cline{1-1}\cline{3-3}\hilne 1 & & 3 \\ \hline \end{tabular}

残念ですね。
2013/12/10
XML を変換するなら Haskell(HXT) で
思えば自分にとってプログラミングとは、XML っぽいドキュメントを操作することでした。 XML パーザを書いてみたり、SXML でいろいろがんばったり、やる前から XSLT に挫折したり。 とにかく気持ちよく XML を操作する方法をこれまでいろいろ探してきたわけですが、今はこう断言できます。 XML をいじるのに最高な道具は HXT(Haskell XML Toolbox)であると。 そこで自分用のメモを兼ねて、「こんなふうに HXT を使ってます」を紹介しようと思います。 基本的な Haskell 力が高くないので、「理解を間違っている」とか「もっとこう使うべき」というツッコミ歓迎です。
HXT がうれしい理由
XML のような木構造のデータをいじろうと思ったとき、まず思いつくのは、根っこから順番に要素をたどりつつ処理していく方法です。 しかし、この方法は案外と融通が利きません。 常に部分木だけを見ればいい仕事なら気楽なのですが、親の要素に戻ったり、兄弟要素にアクセスしたりする必要があるときは、 「いま木全体のどこを処理してるか」という情報を持ち歩くしかないからです。 そして、「いま木全体のどこを処理してるか」を意識しながらコードを書くには、かなりの精神力が要求されます。
HXT では、これとはちょっと違う方法で XML の木構造をいじれます。 「木のこの部分を何かに変換」とか「木のここを取り出す」とか、そういう個別の処理を組み合わせることで全体の変換処理を書けるのです。 同様のアプローチをとるツールとしては XSLT がよく知られていますが、 HXT では各処理を「ふつうのプログラミング言語」である Haskell のコードとして書けるので、 はるかに柔軟だといえると思っています(なにせ XSLT に挫折しているので断言するほどの自信はない)。
組み合わせたい各処理は、「XML の木」から「XML の木」への関数だと思えます。それらをいろいろ組み合わせたいのだから、これは「XML の木構造、および、XML木→XML木な関数たち」から「XMLの木構造、および、XML木→XML木な関数たち」への対応だと思えます。というわけで、 HXT では変換処理を Haskell における圏の間の関手 >>>
を使ってつなげたりできます。
とはいえ、 >>>
だけだと条件分岐もできないし、大して面白い組み合わせが書けないので、 HXT ではもうちょっと広い概念である「アロー」を使って XML 木の変換を組み合わせられるようになっています。アローについて詳しくは『関数プログラミングの楽しみ』の第10章を読んでください(言うまでもなくこれは宣伝です)。
変換を組み合わせる前に
まずは変換処理が一つしかない場合から書いてみます。
<div class="chapter">...<div>
という要素を <h1>...</h1>
に包み直すという、単純な変換がやりたいとしましょう。
構造を変えるのではなく、属性に応じて要素名を書き換えるだけの処理です。
なにはともあれ、最初に HXT まわりのモジュールを読み込みます。
import Text.XML.HXT.Core import Text.XML.HXT.Arrow.XmlArrow import Control.Arrow
これから変換処理をアローとして書いていくわけですが、外界とのやり取りにもアローを使います。
ここでは、ファイルなどの URI を受け取って入力を行うアロー readDocument
と、同じく出力を行うアロー writeDocument
を使います。
それぞれ入出力の書式などを指定することもできて、 DTD の指定やインデントの有無を設定できるのですが、細かいことはドキュメントを参照してください。
さらに、 main
の中でアローを実行するために、 runX
という関数を使います。
readDocument
、 writeDocument
、 それにこれから書く予定の「div
を h1
に変換する」アローである chapterToH1
という三つのアローをすべて >>>
でつないで一つのアローを作り、
それを runX
で実行するだけの超単純な main
は、こんな感じになります。
main :: IO () main = do runX (readDocument [] "test.html" >>> chapterToH1 >>> writeDocument [] "result.html" ) return ()
それでは、肝心の変換処理 chapterToH1
を書いてみましょう。やりたいことは、「もし div
が class
属性に chapter
を持っていたら要素名を h1
にして、 class
属性はいらないので消してしまう」です。これはそのまま、 XmlTree
から XmlTree
へのアロー ArrowXml
として、こんな Haskell の関数で書けます。
chapterToH1 :: (ArrowXml a) => a XmlTree XmlTree chapterToH1 = processBottomUp (ifA (hasName "div" >>> hasAttrValue "class" (=="chapter")) ((setElemName $ mkName "h1") >>> removeAttr "class") (this))
狙いの div
がどこに出てくるかわからないので、 processBottomUp
を使って木を再帰的に見るようにしています。
ifA
は条件分岐のためのアローです。条件にマッチしない要素はそのまま残しておきたいので、 else に相当する部分では this
アローを設定しています(もしマッチする要素だけ残したいなら none
というアローを使います)。そのほかのアローの役割は関数名でわかりますね。こんな具合にアローをつなげて、目的の処理を表すアローを書くわけです。
Haskellのパワーを活用する
先ほどの例と同じ要領で「div
要素を class
属性に基づいて都合のいい名前の要素に変換する」アローをコピペで量産し、それら全部を >>>
でつなげるだけでも、それなりに役に立つXML変換プログラムが書けそうですが、それはつらいので、できればアローを作る関数を一つだけ作って、それを使って作ったアローたちを >>>
で fold できないものかなと考えますよねふつう。実際、アロー版のfoldといえる seqA
が用意されているので、こんなふうに書けます。
(seqA . map (uncurry divClassToElem) $ [("chapter", "h1") ,("section", "h2") ,("para", "p") ])
これを runX
の中に書けばいいわけです。 divClassToElem
はこんなふうに定義すればいいでしょう。
divClassToElem :: (ArrowXml a) => String -- if DIV is this class, -> String -- turn that into this element. -> a XmlTree XmlTree divClassToElem cls elm = processTopDown (ifA (hasName "div" >>> isClass cls) (tameClass elm) (this)) tameClass :: (ArrowXml a) => String -> a XmlTree XmlTree tameClass elm = (setElemName $ mkName elm) >>> removeAttr "class" isClass :: (ArrowXml a) => String -> a XmlTree XmlTree isClass val = hasAttrValue "class" (==val)
兄弟をまとめる
もうちょっと現実的な例題として、いかにも組版ソフトが吐き出したっぽい HTML を、それなりに整った HTML に変換したいとします。つまり、こんなデータを……
<html> <head></head> <body> <div class="chapter">XSLTを捨ててHXTを使おう</div> <div class="para">HXTでXSLTの処理系さえ実装できる!</div> <div class="section">Allowとは</div> <div class="para">こまけーこたーどーでもいいんだよ。</div> <div class="section">HXTの使い方</div> <div class="bulletA"><span class="shell">cabal install hxt</span></div> <div class="bulletB"><span class="haskell">import Text.XML.HXT.Core</span></div> <div class="bulletB"><span class="haskell">let doc = readString [] text</span></div> <div class="bulletC"><span class="haskell">runX doc</span></div> </body> </html>
こんなふうに変換したいとします。
<html> <head/> <body> <h1>XSLTを捨ててHXTを使おう</h1> <p>HXTでXSLTの処理系さえ実装できる!</p> <h2>Allowとは</h2> <p>こまけーこたーどーでもいいんだよ。</p> <h2>HXTの使い方</h2> <ul> <li> <code>cabal install hxt</code> </li> <li> <code>import Text.XML.HXT.Core</code> </li> <li> <code>let doc = readString [] text</code> </li> <li> <code>runX doc</code> </li> </ul> </body> </html>
この例で面倒なのは、 class
属性の値だけに基づいて、元の XML で潰れてしまっている構造(この場合は ul
要素)を取り出さなければならないところです。
面倒とはいっても、リストみたいなデータ構造から「bulletA」~「bulletなんとか」までの連続する要素をとってくるような問題そのものは、Haskell であれば Data.List
の groupBy
を使って割と単純に解けてしまいます。
特定の属性の値という条件のままで考えるとちょっと込み入ってしまうので、代わりに要素名がそれぞれ「bulletなんとか」になってると単純化して考えてみます。
HXT なら、要素名を属性の値に変換した木を作るアローを作って前段にかませばいいだけなので、こうみなしても後で困ることはありません。
import qualified Text.XML.HXT.DOM.XmlNode as XN import Data.List groupBullet :: [XmlTree] -> [XmlTree] groupBullet ts = map bulletlines $ groupBy isBullet ts where bulletlines [x] = x bulletlines a@(x:xs) = XN.mkElement (mkName "ul") [] a isBullet :: XmlTree -> XmlTree -> Bool isBullet t1 t2 = case (XN.getElemName t1, XN.getElemName t2) of (Just x', Just y') -> let x = qualifiedName x' y = qualifiedName y' in (isPrefixOf "bullet" x) && (isPrefixOf "bullet" y) && (not $ isPrefixOf "bulletA" y) (_, _) -> False
この groupBullet
はアローではなく、 XML 木から XML 木への単なる関数です。あとでアローに持ち上げます。
その前に、「bulletなんとか」という class
属性を持つ要素の名前を、その属性の値に変換するアローを書いておきましょう。
これがないと、いま定義した groupBullet
関数を使っても意味がありません(だって、元の木では要素名が全部 div
ですから)。
classValToName :: (ArrowXml a) => String -> a XmlTree XmlTree classValToName cls = setElemName $< ((isClassPrefixOf cls >>> getAttrValue "class" >>> arr mkName) `orElse` getElemName) isClassPrefixOf :: (ArrowXml a) => String -> a XmlTree XmlTree isClassPrefixOf val = (hasAttrValue "class" (isPrefixOf val))
この classValToName
というアローを使って、次のようなアローを main
の runX
の中に仕込めば、つぶされていた構造を ul
要素として取り出せます。
processTopDown (((getChildren >>> classValToName "bullet") >>. groupBullet) `when` (hasName "body"))
ここでポイントになるのが >>.
という演算子です。
この >>.
演算子は、前段にあるアローの行先に対して、リストからリストへの関数を適用したアローを返します。
さきほど定義した groupBullet
は [XmlTree] -> [XmlTree]
な関数だったので、 >>.
でアローと接続することで、「bulletなんとか」をグループにして ul
でくくるというアローになります。
なお、あからさまな条件を when
で指定して全体をわざわざ囲っているのは、こうしないと groupBullet
したい要素たちを取ってこれないからです。
最後に、 class
属性の値が「bulletなんとか」の div
要素をすべて li
に変換しましょう。
基本は最初に書いた chapterToH1
とまったく同じですが、今度は条件分岐に choiceA
アロー使い、上で定義した isClassPrefixOf
を条件にしてこんなふうに書いてみました。
bulletToLi :: (ArrowXml a) => a XmlTree XmlTree bulletToLi = choiceA [isClassPrefixOf "bullet" :-> (tameClass "li"), this :-> this]
この例の全体を gist に張っておきます。
まとめ
ここでは、ほとんど >>>
を使うだけで済む例しか出てきませんでしたが、実際にはかなりたくさんのアローの組み合わせ方が HXT で提供されています。基本的な XML の操作に便利なアローも、あらかじめたくさん実装されています。さらに、 >>.
のようなリストとの連携機能も豊富です。おかげで、 Haskell の関数としてなら容易に書けるような処理を、 XML 木の変換をするアローとしてそのまま使えるようになります。
今回の記事で最後に書いたような HTML データの簡単な変換処理は、編集の現場ではちょくちょく遭遇します。 実際、 InDesign のような DTP ソフトが吐き出す XML を整形するのに、 HXT を利用して同じようなスクリプトを書いています。 些細な原稿データを一回だけ変換するなら、エディタの正規表現による置換で済ませてしまうほうがお気楽ですが、もっと複雑だったり素性が知れなかったするXMLデータを繰り返しいじるなら、 HXT も選択肢にいれてあげてください。
というわけで、去年に引き続き、今年の Haskell Advent Calendar 2013 も「編集者のための Haskell 入門」をお届けしました。
2013/12/02
TUG 2013で日本語の巻末索引について発表しました
TUG 2013という、世界中からTeX関連の開発者と利用者が日本に集まるイベントで、日本語書籍の索引について発表する機会がありました。「発表してみないか」と実行委員の黒木さんに誘われたときは、仕事でやってることを10分くらいで紹介すればいいのかなと思って軽い気持ちで引き受けたのですが、実は「海外からの参加者向けに用意する日本語チュートリアルの一環なのでよろしく」という話でした。当日は早口でいろいろ詰め込んでしまったせいか、話の筋を見失った方もいると聞いたので(すみません)、いまさらですが発表のストーリーをまとめておきます。(TeX & LaTeX Advent Calendar 2013の2日目の記事です。1日目はzrさん、3日目はdoraTeXさん。)
- いろいろな言語の本の索引を比べてみると、索引項目(一般には単語)の「自然な」並べ方がある言語とない言語がある
- アルファベットを使う言語(ラテンアルファベットだけでなくハングルも含む)や、広義のアルファベットを用いる言語(タイ語とか)では、文字は言語によっていろいろだけど、単語は字面だけを見て並べられる
- 一方、アルファベットを持たない日本語と中国語では、単語の並べ方に工夫が必要
- そこで、日本語の本における索引の並べ方を概説
- いわゆる50音順は、文字の順番じゃなく、シラブルの順番
- 中国語の辞書などで採用されてるピンイン順との比較
- 人名を並べるときなど、50音順を破って字面優先にする場合(いわゆる電話帳順)もあるよ
- さらに具体的に、LaTeXで日本語の索引をどうやって作ってるか
- makeindexやxindyは使えないので、日本語専用のmendexというツールを使うしかない
- 漢字の読み仮名を機械的に解決するには、辞書だけじゃなくて、わかち書きのために形態素解析器も必要
- 原稿がマークアップで汚くなるのを回避するために、gitで索引用のブランチを作るといいよ
- いろいろ面倒だけど、索引重要
- 索引は、本文から単語を探すツールであるだけじゃなくて、本文の圧縮版としての役割もあるんだよ
- 本を書いたり編集したりする人は、どうせ書いたものを何度も読むことになるんだから、そのうちの一回を「索引を作る」にあてるべきだよ
- 電子書籍には電子書籍の索引の形があるはずなので模索していくべき
発表後、主に欧米の方々から個別にいろいろ質問されました。
Q:索引項目は文字列として短いのに、それでも形態素解析器が必要なの?
A:日本語は、まず分かち書きをしないと辞書すらひけないから、そのためだけでも必要。
Q:バージョン管理には何を使ってるの?
A:git(実際にはgit-svnが多い)
Q:たとえば「山」の字は「さん」と「やま」と二通り読むらしいけど、索引ではどっちに載せるの?
A:どういう単語に「山」の字が含まれてるかによって、たいていは一通りに決まる。決まらない例として、「空リスト」は「からりすと」と「くうりすと」とが毎度議論になるけど、自分は両方載せちゃう。
Q:索引をちゃんと作ったほうがいいというお前の話はまったくそのとおりだ
A:ありがとうございます!
Q:xindyは文字の順番を定義できるけど、なんで日本語では使えないの?
A:設定ルールがアルファベット前提っぽいから、少なくとも何らかの工夫は必要そう。それ以上詳しいことは今はちょっとわからない。
Q:中国語の索引は画数って話だけど、同じ画数の漢字の順番はどう決まるの?(実際には質問されなかったけど、勝手に想定してた質問)
A:筆順で決まる。「横線」「縦線」「はらい」「点」「折れ」の順(らしい)。
英語で発表というのもあって、準備はものすごい大変だったけど、終わってみればとても面白かったです。声を掛けていただいた黒木さん、本当にありがとうございました。事前の練習にお付き合いいただいた関東在住のTeXユーザーの皆様にも本当にありがとうございました。
本音を言うと、「日本語チュートリアル」という4つしかない枠の1つをまるまる「索引」に割り当てるのは、はっきりいって頭がおかしい構成ではないかと思っていました。今でも思います。もしかしたら、日本で開催するTeXの国際会議の一コマとして、コンピュータによる日本語の「排列」(用語の標準的な並べ方)の話題に絞るほうがよかったのかなあとも思います。とはいえ、例えば上記のような質問は「(巻末)索引」という切り口だったからこそ出てくる問題なのだろうし、そもそも日本語の排列の一般論なんて自分にはお手上げだし、自分なりに全力は尽くしたと思いたい。
TUG 2013 雑感
内外のいろいろな方にお会いして、中古カメラの話とか子どもの話とか、TeX以外にもいろいろと盛り上がれてとても楽しかったです。
とくにフランク・ミッテルバッハさんがとてもかっこよく、すっかりファンになりました。LaTeXコンパニオンにサインもしてもらいました。自分の発表後に「お前の話はまったくそのとおりだ」といってくれたのも彼です(その前にプロジェクタの接続を手伝ってあげたのでリップサービスかもしれませんが気にしない)。これからはLaTeX3を使っていこうと思います。
2013/11/04
Personal Impression on TUG 2013
For me, TUG 2013 had started August 2012, when Yusuke Kuroki, a mastermind behind Japanese TeX users, asked me to describe indexes of Japanese books at the next TUG, which would be held in Tokyo. Having intended to apply for any presentation slot, over whatever topic, I agreed to his request. I didn't think that I'm a sort of authority on this topic, but in the context of TeX, I was sure that I had enough experiences on indexes.
So I undertook a brief tutorial on making back-of-the-book indexes using TeX. What I didn't expect was that the tutorial would be over an hour long. I wasn't sure if indexes in Japanese actually would be much interesting for the most of the attendees. In addition to that, I thought the whole tutorials seemed to be overly focused on the compositions and typesettings of Japanese, rather than TeX. Until the TUG 2013 actually started, I had been a little bit skeptical how much non-Japanese speakers would enjoy the whole tutorials.
My concerns proved to be unfounded. The art of formatting Japanese is likely to be realized by the non-Japanese TeX users, let alone by the developers of the various typesetting systems. It would be nice these tutorials let everyone think about what we need to avoid awful looking documents. To put it bluntly, that is one of the raison d'etre of TeX.
Of course, there's another reason why we use TeX. That is because TeX allows us to take advantage of computers in creating documents, unlike word processors which requires a bunch of hand work. And in this context too, there is some subtle point in regard to Japanese that we wanted to let the attendees know. I can't help but feel partially responsible for that. I think both my talk on arranging index entries and Kuroki-san's additional tutorial on input methods revealed the gimmicky way of handling Japanese in the computer and TeX.
As a matter of fact I'm not good at speeches even in my native tongue. I felt as if my stomach was yanked out of my mouth during my presentation. But finally I found myself enjoying that. It was the great experience to hand on my knowledge to the people, including legendary developers around the world. I was really excited when I was directly questioned by them after the presentation. Again I was upset, this time the other way round.
Anyway, I reckon TUG 2013 was the great international conference. I'd like to thank the organizers for their energetic effort, and my particular thanks to Kuroki-san for giving me a rare opportunity.
2012/12/30
2013年賀状
年賀状書いた。
%! << /PageSize [285 420] >> setpagedevice % excerpt from Bill Casselman's % "Mathematical Illustrations - a manual of geometry and PostScript" /ctransform { load 1 dict begin /f exch def [{[3 1 roll f {moveto}]} {[3 1 roll f {lineto}]} {[7 1 roll f 6 2 roll f 6 2 roll f 6 2 roll {curveto}]} {[{closepath}]} pathforall ] newpath {aload pop exec} forall end } def /f { % u = (x^2-y^2) / 200, v = 2xy / 100 /y exch def /x exch def x dup mul y dup mul sub 150 div 1.2 x mul y mul 100 div } def /fcn { % u = ((x-100)^2-y^2) / 7 , v = 1.2xy / 7 /y exch def /x exch def x 100 sub dup mul y dup mul sub 28 div 1.2 x mul y mul 7 div } def /setrandcolor { /m exch .4 add def /r1 {rand 1 mod 1 div m add} def /r2 {rand 3 mod 17 div m add} def /r3 {rand 10 mod 17 div m add} def r1 r2 r3 setrgbcolor } def /randcgrid { currentlinewidth 10 div setlinewidth 0 .5 300 { /i exch def i 0 exch moveto 300 0 rlineto /f ctransform i 400 div setrandcolor stroke} for 0 .5 300 { /i exch def i 0 moveto 0 300 rlineto /f ctransform i 300 div setrandcolor stroke} for } def /xoffset 200 def 0 1 20{ /n exch def xoffset 10 add -10 translate randcgrid 0 xoffset sub 20 translate 0 0 moveto newpath /Titania findfont 11 scalefont setfont 0.7 0.1 0.1 setrgbcolor 12 5.5 moveto (2008 2009 2010 2011 2012 2013 2014) true charpath /fcn ctransform clip /fcn ctransform clip 1 0.5 0.55 setrgbcolor fill stroke newpath 1 0 0 setrgbcolor 120 setlinewidth 145 140 moveto 145 400 lineto stroke initclip 20 10 moveto /Georgia-Bold findfont 10 scalefont setfont 0 0 0 setrgbcolor n =string cvs show (/20) show showpage 0 0 moveto } for
"Mathematical Illustrations"を読みつつパスを変換(実質的には等角写像でぐにゅっと曲げた)して生成した。/fcn
が西暦の文字列を構成するパスを変換するための写像を表す関数で、/f
が矩形の格子を背景のテクスチャへと変換する写像を表す関数。
利用したフォントは Titania 。毎年、フォント探しに一番時間がかかってるような気がする。
毎年、親戚や家族付き合いのある友人向けに20枚くらいだけ印刷してて、今年のものは「2008年に生まれた息子も5歳を迎えます今年も来年も引き続きよろしくお願いします」と読みます。去年のものはだいぶ色きちがいになってしまったので、今年はあっさりとした配色にして、何か手書きするスペースを残しました。
2012/12/24
TeX で花火(手抜きバージョン)
ちょうど一年前、「LaTeX/TikZ で花火」というネタが流行ったのは記憶に新しいところです(参考1、参考2)。 TikZ は高機能なグラフィック機能を提供する LaTeX のパッケージで、繰り返し構文なども提供されており、アルゴリズム的に花火っぽい絵を描画するコードがいくつか登場しました。
自分でも TeX & LaTeX Advent Calendar 2012 のために花火をひとつくらい打ち上げてみようかなと思ってたのですが、TikZ と格闘するような時間もなく、あきらめてかけていたところ、この花火ネタより前の2009年に話題を集めた LaTeX お絵かきネタを思い出しました。コーヒーポットのシミを論文などに刷り込むための LaTeX スタイル "LaTeX Coffee Stains" です。これの花火バージョンがあれば、なんとかゴール目前までつながった TeX & LaTeX Advent Calendar 2012 のお祝いネタとしてちょうどいいのではないでしょうか。
というわけで急遽でっちあげたのが、見た目にもめでたい「花火をページに刷り込むコマンド」です。花火を打ち上げたいページで \fireworks
と書くだけ!
正直に打ち明けると、このネタは上記の coffee.sty の丸ぱくりです。 しかも自分では TeX のコードをまったく書いてません。元ネタである coffee.sty を見たら、PSTricks という PostScript の描画命令を直接埋め込める機能を使ってコーヒーのシミを描いていたので、この PostScript のシミデータを自作の PostScript 花火データにまるっと差し替えました。 PostScript の花火をどう作ったかは聞かないでください。 PSTricks では PostScript の描画コマンドは直接利用できるけど PostScript のプログラムを書けるわけではないので、アルゴリズム的には生成してないです。なお、 PSTricks は dvipdfmx では使えないので、PDFを作る場合は pdftex か dvips 経由で。
というわけで、明日の TeX & LaTeX Advent Calendar 2012 最終日はTeX芸人の中のTeX芸人、ZRさんの予定です。
2012/12/18
parsec で極める文章編集
正規表現をまったく使えない編集者はひとにぎりだと思いますが、正規表現だと原稿の半角丸括弧を全角に変換する作業とか頭痛いですよね。わたしもいつも困ってました。
というわけで、いまや編集者必須ツールといってもいい parsec を新人編集者にぜひ使ってもらおうということで、 「Haskell Advent Calendar 2012」18日目という場を借りた素人チュートリアル記事です。 Haskeller が書いてるわけではないので、 「その考え方は違う」とか「もっと効率的な書き方がある」といったコメントがもらえるとうれしいです。 ちなみに、わたしの周りに新人編集者はもう何年もいません。まだ見ぬ新人へ向けて書きます。
parsec で最速テキストフィルター
最初に parsec を使おうと思ったときにぶちあたるのは、プログラマ向けの解説しかないことだと思います。 編集者というものは、 CSV や IP アドレスをパースしたり、ましてや関数電卓を作ったりしない。 Ruby で正規表現を使ったテキストフィルターを書くときみたいに parsec を使うにはどうすればいいでしょうか。
外側から考えます。いま作りたいのは、入力ファイルを指定して、何か文字列変換を施した結果を出力するテキストフィルターです。Ruby なら ARGF.gets
とかすればいいとこですが、ここは我慢してとりあえずこんな枠組みを書きます。
module Main () where import System.Environment import qualified System.IO as IO import Text.ParserCombinators.Parsec hiding (many, (<|>)) import Control.Applicative main = do args <- getArgs inh <- IO.openFile (args !! 0) IO.ReadMode body <- IO.hGetContents inh IO.putStr $ doSomething body IO.hClose inh
doSomething
が具体的な変換処理で、ここに「日本語文章に出てくる半角丸括弧を全角に」とか「TeX の数式を抜き出す」とか「コード行に出てくるキーワードにハイライトのタグをつける」とかいった処理をするパーザを parsec で書くわけです。
では doSomething
を考えましょう。ここでは doSomething
という名前を説明のために使い続けますが、実際にスクリプトを書くときはテキスト変換処理を表す適切な名前をつけてください。
doSomething :: String -> String doSomething lines = case parse (concat <$> manyTill block eof) "" lines of Left err -> "" Right str -> str
テキストフィルターなので doSomething
は文字列から文字列への変換を担う関数でないと困ります。
そこで 1行目には String -> String
と書いてあります。
case ... of
の内側の manyTill block eof
が、とっかかりとなる最初のパーサです。
これは、「今はなんだか決めてないけど block
という文字列の塊を取ってくるパーザがあるとして、それをファイルの終わり eof
まで繰り返し実行する(manyTill
)」、という意味です。
繰り返しとってきたその結果は、文字列のリストなので、一つにつなげるために concat <$>
と書いています。
このように関数のうしろに <$>
と書き、続けてパーザを書くと、「うしろに書いたパーザが返すものに最初の関数を適用したものを返すパーザ」になります。
ここで注意しないといけないのは、パーザは「文字列をパースして得られる文字列」を返してくれるわけではないという点です。
parsec では、パーズして得られる結果は「パーザを実行する専用世界」の中にあり続けます。
その専用世界を Parser
と呼ぶことにしていて、
だから parsec におけるパーザは、たとえば文字列を返すものであれば Parser String
といいます。文字を返すものなら Parser Char
です。何も返さないパーザというのもあって(スペースを読み飛ばす、とかです)、これは Parser ()
といいます。
この Parser
世界の中にフィルタリングしたい文字列を入れて、パーズした結果をもらいたいわけですが、この世界とのやり取りは決められた出入り口からしかできないようになっています。
someParser
というパーザを書いたとして、それで文字列をパーズして結果を「専用世界」の外に引っ張り出してくる方法のひとつが、上記の parse someParser "" lines
という書き方です。
こうして手に入る結果は「パースに成功してこんな文字列が手に入った、または失敗した」というちょっと変わった形をしています。そのままでは doSomething
の結果としてふさわしくありません(だって doSomething
は文字列を返すってことにしたので)。そこで case ... of
と Right
および Left
という識別子を使って、成功の場合も失敗の場合も文字列を返すようにしています。作ってるのがテキストフィルターなので、失敗の場合は空文字列を返しとけばいいでしょう。
ここでようやく block
を何にするか決めます。
テキストフィルターの仕様を考えるわけです(というわけで、ここまではテキストフィルターを書くときの定型だと思ってもいいです)。
いま、HTML の <p>
タグの内側にある半角丸括弧だけをすべて全角丸括弧に直したいとします。
一方、 <pre>
タグの中にある半角丸括弧とかは、コードの断片である可能性が高いので、変換してはいけないとします。数式なんかに出てくる半角丸括弧も全角にしてはいけません。
いま仮に、 <p>
タグの中には日本語の本文だけしかないものとしましょう(でないと説明のコードが増えてしまうからです。べつに一定のルールにしたがって出てくるぶんには、その部分だけ処理を飛ばすようにパーザを書けばいいのです。ただし完璧を目指すと泥沼になるので適当な精度で切り上げましょう)。
この場合の block
の仕様はこうです。こいつは文字列を返すパーザにしたいので、 Parser String
だと宣言しておきます。
block :: Parser String block = choice [ try japara , otherlines ] japara :: Parser String japara = string "<p>" *> (conc <$> manyTill anyChar (try $ string "</p>")) where conc = ("<p>"++) . (++"</p>") . replaceParen otherlines :: Parser String otherlines = manyTill anyChar $ (try $ string "\n")
ざっくりというと、日本語の本文(japara
パーサ)かそれ以外(otherlines
パーサ)かで選択(choice
関数)をして、日本語の本文だったら半角括弧を全角に変換します(replaceParen
関数)。
block
だけでなく、そこから呼んでる japara
だったり otherlines
だったりは、すべて文字列を返すパーザです。こんなふうに、基本的なパーザをいろいろ組み合わせて好きなパーザを作るわけです。 string [文字列]
とか anyChar
なんかも、もちろんパーザで、これらのいわば最小の部品は parsec にあらかじめ用意されています。ほかの部品はここにドキュメントがあるので探してください。
japara
、つまり日本語の本文は、「文字列 <p>
から </p>
までの内側」です。 *>
は、右側だけを結果に残すようなパーザを作ってくれます。
otherlines
、つまり日本語の本文以外は、「改行までの文字なら何でも」とってくるパーザです。
この定義だと 1行とったら終わってしまうように思いますが、外枠のほうで block
を何度も繰り返しとり出すことにしてあるので、これで問題ありません。
<p>
タグ内で半角括弧の置換を行う replaceParen
を書くには、
最初のほうで定義した doSomething
と同じ考え方をします。
doSomething
では本文全体から必要なブロックと不要なブロックを切り出すパーザを繰り返し使ったわけですが、今度は置換する要素としない要素に切り刻むパーザを作り、それで各ブロックを処理していきます。
丸括弧は入れ子になってるかもしれないので、丸括弧か否か(parens
と noParens
)だけでなく、丸括弧内か(inParens
)も選択肢になりえます。
replaceParen :: String -> String replaceParen line = case parse (concat <$> many1 strOrParen) "" line of Left err -> "" Right str -> str strOrParen :: Parser String strOrParen = choice [ try noParens, try inParen, parens ] inParen :: Parser String inParen = string "(" *> (wrapDP <$> (manyTill strOrParen (string ")"))) where wrapDP = ("("++) . (++")") . concat noParens :: Parser String noParens = many1 $ noneOf "()" parens :: Parser String parens = many1 $ oneOf "()"
以上を ReplaceParen.hs
のような名前で保存して以下のように実行すれば <p>
タグの中だけ半角丸括弧を全角に置換できます。このブログ記事のソースみたいなのを処理しても、コード片に出てくる半角丸括弧は置換されません。やったね。
$ runghc ReplaceParen.hs input.html > result.html
parsec でテキストフィルターを書くときのまとめ
- 全体をブロック要素へと切り刻むパーザを choice で作り
- 各ブロックをインライン要素へと切り刻むパーザを choice で作る
- どちらも 「
case parse [パーザ] "" [パーズする対象] of
」で文字列から文字列への関数にしたてる
書き捨てとはいえ、このような再帰的なパターンになると、正規表現をサポートしてるエディタではつらいし、sed/Ruby/Python/Gauche などでスクリプトを書くにしてもかえってコード力が要求されることが多いように思います。 単純よりちょっと込み入ったテキスト処理になると、Haskell のほうが parsec のおかげで楽に編集補助ツールが書けることもあるはずです。プログラマでないみなさんも『すごい Haskell たのしく学ぼう!』と『プログラミング Haskell』だけは読んでおきましょう。再帰に対する理解もあるとなおよいので、『Scheme 手習い』もぜひ読みましょう。これは宣伝です。
Haskell のコードには、ここで出てきたような <$>
とか *>
のような記号がちょこちょこ出てくるのでとっつきにくいかも知れませんが、『すごい Haskell たのしく学ぼう!』の Kindle 版とか、ちゃんと索引もついてるので、こんな検索しにくそうな記号もばっちり調べられます。これは宣伝であると同時に、電子書籍にも索引あったほうがいいよという、この記事の対象者である新人編集者むけのアドバイスです。
注意
書き捨てのテキストフィルターに完璧は目指さないこと。あくまでも編集作業の補助に使いましょう。
実は上の例でも、たとえば原稿中に <p>
要素の入れ子がないことや、<p>
タグがすべて行頭にあることを密かに仮定していて、だから otherlines
が簡単に定義できてます。
経験からいうと、 otherlines
のような「探してないその他大勢」をすっ飛ばすパーザを書くほうが大変で、行頭とか空白とか特殊文字といった都合のいい条件のない完璧なパーザを目指そうとすると、とたんにスクリプトが巨大になります。この例だと、それこそ XML パーザを書く勢いが必要です。しかし、いまほしいのは書き捨てのテキストフィルターです。妥協が肝心です。ビルドスクリプトに組み込むとかでなければ、一発で完全に処理しようとしないほうが幸せです。
2012/12/15
TeX がむかついたので実装したけど挫折してる話
いきなり私見ですが、「TeX むかつく」という声の大半は、「インストールが苦痛」または「マクロが意味不明で思い通りならない」に分類できるんじゃないでしょうか。 でもまあ、便利なインストーラ付属の分かりやすいLaTeX2eの入門書が改訂を重ねて書店で売ってたりするのだし、これらは普通の用途においては解決策がある問題ということにしておきましょう。
インストールの壁をクリアして、ちょっと凝ったページの出力もできるようになると、 基本的な組版機能に不足はないし、数式にいたってはほぼ唯一無二だし、自作マクロでちょっとした自動化さえできるわけで、 「とにかくTeXむかつく」という心境から、「いっけん意味不明な制約やなぞの挙動には目をつぶるとして TeX しかないな」という心境に至る人も多いはずです。 わたし自身も、組版に関しては、だいたいそういう心境です。残念ながら TeX しかない。
しかし、それでも TeX には実際むかつくことがある! それは、「TeX がドキュメントシステムに向いてない」ことです。「自由すぎる」ともいえます。
ドキュメントというのは、木構造になっているべきです。 キーワードやフォント指定用語などのインライン要素から構成される章、節、段落、図、数式といったブロック要素が、根っこで一つのドキュメントへと束ねられてるようなイメージです。 散文一般とドキュメントに区別があるとしたら、この木構造が必須か否かという点かもしれません。 ドキュメントは、なんらかの木構造と一対一対応がないと、最悪、読み手によって意味が変わってしまいます。 小説ならそれでいいんですが、たとえばジェット機の整備マニュアルなんかだと、それではまずい。 だから、まっとうなドキュメントには木構造が求められるわけです。XMLとかSGMLが重宝されるゆえんです。
だからドキュメントをシステマチックに扱うには、木構造を反映できるファイル形式が不可欠です。 さらに、ドキュメントを木として編集するための仕組みもあればうれしいでしょう。
TeX がドキュメントシステムに向いてないと思うのは、この木構造の観点が欠落しているからです。 もちろん、書き手が注意して執筆することで、 TeX を使って木構造のドキュメントを作ることはできます。 しかし、 TeX で執筆された原稿を扱う汎用の仕組みを作ろうとすると、構文解析すら無理ゲーなことに気づきます。 ドキュメントシステムの処理系を作ろうとしたら、原稿から木を作ってそれを内部表現として扱うと思うのですが、 TeX の場合、原稿を書くための基本的な構文も内臓のマクロ言語によって提供されているため、原稿から木を作りたいだけなのに、原理的にはこのマクロ言語の処理系を書かないと完全なものにならない。そこで実践では「サポートする TeX の構文」を決めうちすることで TeX マクロ言語処理系を実装する手間は省くことになってしまい、だから世の中には「汎用の LaTeX→XML コンバーター」のような仕組みが見当たらないのです。
ないから作った
ないなら作ればいいというわけで、TeXマクロ言語の処理系を Gauche で実装してみたことがありました。TeX-modoki といいます(その後「tef」に改名)。基本的なマクロの展開が扱えます。久しぶりにソース見たら \afterassignment
も \aftergroup
も未実装だったけど、奇数個の \expandafter
の塊くらいはまじめに処理できます。
tex-modoki$ cat sample/expandafter-test.tex \def\A#1#2#3{#1#2#3a}\def\B#1#2{#1#2b}\def\C#1{#1c}\def\D{d} \expandafter\expandafter\expandafter\expandafter \expandafter\expandafter\expandafter\A \expandafter\expandafter\expandafter\B\expandafter\C\D tex-modoki$ gosh -I. main.scm -o text sample/expandafter-test.tex dcba
トークンのカテゴリコードもまじめに扱ってるので、それ相応の判定結果が得られます。
tex-modoki$ cat sample/ifcat-test.tex \catcode`[=13 \catcode`]=13 \def[{*} \ifcat[* yes \else no \fi % yes \ifcat\noexpand[\noexpand] yes \else no \fi % yes \ifcat\noexpand[* yes \else no \fi % no tex-modoki$ gosh -I. main.scm sample/ifcat-test.tex yes yes no
未実装の機能が多いので制限だらけですが、簡単な整数計算のマクロなら実行できるっぽいです。「スヤァTeX」に登場する、修正ユリウス通日を求めるマクロも、少し修正は必要ですが実行できました(できるように今朝ちょっと実装を追加した)。「\relax」の変わりに「( ˘ω˘ )スヤァ…」を使うには(ry
tex-modoki$ gosh -I. main.scm -o text sample/julian.tex 56286 tex-modoki$ cat sample/julian.tex \input sample/plain % tex-modoki 用に修正した plain TeX マクロを指定 \newcount\xx@cnta \newcount\xx@cntb \newcount\xx@year % 4桁の整数の格納に mathchar を使わない! \newcount\xx@month % 2桁の整数の格納に char を使わない! \def\julian#1{% % LaTeXじゃないので newcommand を def に \edef\xx@tmpa{#1}% \expandafter\xx@julian@a\xx@tmpa///\relax } \def\xx@julian@a#1/#2/#3/#4\relax{% \xx@cnta=#1\relax \xx@cntb=#2\relax \ifnum\xx@cnta<1 \xx@cnta=\@ne \fi % 条件式の \@ne を 1 に \ifnum\xx@cntb<1 \xx@cntb=\@ne \fi % 条件式の \@ne を 1 に \ifnum\xx@cntb<3\relax \advance\xx@cntb by 12\relax \advance\xx@cnta by -1\relax \fi \xx@year=\xx@cnta \xx@month=\xx@cntb \multiply\xx@cnta by 1461\relax \divide\xx@cnta by 4\relax \xx@cntb=\xx@year \divide\xx@cntb by 100\relax \advance\xx@cnta by -\xx@cntb \divide\xx@cntb by 4\relax \advance\xx@cnta by \xx@cntb \xx@cntb=\xx@month \advance\xx@cntb by -2\relax \multiply\xx@cntb by 520\relax \divide\xx@cntb by 17\relax \advance\xx@cnta by \xx@cntb \advance\xx@cnta by #3\relax \advance\xx@cnta by -678912 \edef\thejulian{\number\xx@cnta}% } \julian{2012/12/25} \thejulian
というわけでこの記事も TeX & LaTeX Advent Calendar 2012 の15日目に向けて書きました。12日ぶり2回目。
TeX の数式を HTML で出力
さらに副産物として、「素TeXで書いた数式の HTML 出力」ができます! もともと TeX の組版機能を実装するつもりはなく、その力もないのですが、TeX の数式を木構造へと変換できるところまでいったので HTML+CSS でも出力できるようにしてみました。
出力例はこんな感じで、残念なとこもあるけど、まあ見られるかなというレベル。
ソースは、こちらのような、単純な TeX ファイルです。 しかし、 LaTeX はもとより、ごく基本的な TeX の標準マクロさえないので、手元にある LaTeX の数式を変換しても内部表現を吐き出すだけだと思います。
結局挫折
けっこうがんばってここまで作ってたのですが、今では開発をほぼ中止しています。なぜなら、TeX では字句解析時の文脈もマクロからいじれてしまう(ヘンタイ!)という事実に後から気が付いたので、完全なものにするにはゼロから作り直す必要があるからです。TeX むかつく!
数式変換機能の線でもうちょっとがんばって開発を続けようかなと思ったこともありましたが、 LaTeX や ams の膨大な遺産を前に挫折! やっぱり TeX むかつく!
2012/12/03
TeXを電卓として使おう(あるいは、TeXでベキ乗根)
プログラミング言語 TeX にはインタプリタがついてきます(ツッコミ禁止)。そこで他の多くの言語のインタプリタのように、TeX インタプリタを「電卓」として使ってみましょう。(なお、この記事は TeX & LaTeX Advent Calendar のために書いたものです。明日は @neruko3114 さん!)
まずは TeX インタプリタの起動です。日本語の組版をするなら ptex
を実行するところですが、電卓に日本語はいらないので、今回は世界標準らしい pdftex
を使います。 TeX インタプリタの標準プロンプトは *
(アスタリスク)ですが、起動時だけは興奮気味に **
と2つ重ねて出力されるので、お約束として一言目は毎回 \relax
と入力して TeX インタプリタを落ち着けてください。
$ pdftex This is pdfTeXk, Version 3.141592-1.40.3 (Web2C 7.5.6) %&-line parsing enabled. ** \relax *
さっそく 1+2
と入力してリターンを押したいところですが、TeX で単に数字や演算子を書いてしまうと、通常の文脈ではドキュメントへと印字する文字列とみなされます。いかにもドキュメント処理システムっぽいですね。数字を整数値として扱うには、整数用の \count
レジスタというハコを用意してそこに値を格納し、そのレジスタ上で演算を行います。整数の加算からやってみましょう。
* \newcount\a * \a=1 * \advance\a by 2 * \showthe\a > 3. <*> \showthe\a ? % ここで単にリターン *
1行めの \newcount\a
で新しい \count
レジスタ \a
を用意し、2行めで\a
に整数値の1を代入、 \advance\a by 2
という命令で \a
「2」の値に2を加算し、 \showthe
というコマンドで \a
の中身を出力しています。結果は赤字の「3」です。 \showthe
で結果を表示したあとはプロンプトが ?
に変わりますが、気にせずリターンを押せば元の *
に戻ります。簡単ですね。なお \advance
コマンドの引く数にマイナス記号をつければ(\advance\a by -2
)、減算ができます。減算専用のプリミティブは用意されていません。
続いて、整数の乗算と除算です。それぞれ \multiply
、\divide
という命令が用意されています。先程のセッションに続けて以下のように入力してみましょう(したがって、いま \a
には 3 が入ってます)。
* \multiply\a by 3 * \showthe\a > 9. <*> \showthe\a ? % ここで単にリターン * \divide\a by 2 * \showthe\a > 4.
最後の行は「9÷2」の結果なので「4.5」になるはずですが、 \count
は整数用のハコなので端数は切り捨てられます。キャストとかされません。というか、そもそも実数そのものを扱うハコがありません。
では、結果が実数になるような複雑な計算は実現できないのでしょうか。試しにベキ乗根のつもりで \sqrt\a
とかしてみましょう。
* \sqrt\a ! Missing $ inserted.
$
がない、つまり数式組版モードじゃないと言われました! 当たり前と言えば当たり前なのですが、TeX というのは本来はドキュメントシステムなので、 sqrt はもちろん、 + や * 、 さらには数字にいたるまで、ふつうの言語なら数値やその演算を提供するであろう要素はほとんど組版の機能に利用されてしまっています。
しかし TeX は、「かっこいい出力ができる汎用プログラミング言語」でもあるはずです。ベキ乗根の近似値くらい計算できないはずがない。そこで、ベキ乗根の近似値をかっこよく組版で使うための TeX マクロ \sqrtd
を作ってみましょう。目標は、こんな入力から、
You can get \sqrtd{2}, \sqrtd{2.5}, \sqrtd{3} and \sqrtd{3.14} by Newton's method with initial guess $1.0$.
こんな出力を得ることです。
(2のベキ乗根があまり精度よくないのは目をつぶってください><)
TeXでニュートン法を実装しよう!
ベキ乗根の近似値を得る方法として一番簡単なのはニュートン法でしょう。まずはニュートン法を使って値 x のベキ乗根を求める手順のおさらいです。
- ベキ乗根になりそうな値 y を適当に推測します。
- その推測値 y のベキ乗 y2 と、元の値 x との差を調べます。
- この差が十分に小さかったら、その推測値 y はだいたいベキ乗根に近いということで、答は y です。
- この差が十分に小さくなかったら、推測値 y と x/y の真ん中の値をあらためて推測値 y とし、1.に戻ります。
いきなり TeX はきついので、とりあえず Scheme で書いてみます(詳しくはSICPの1.1.7節を参照)。
(define (sqrt-iter guess x) (if (good-enough? guess x) guess (sqrt-iter (improve guess x) x))) (define (good-enough? guess x) (< (abs (- (square guess) x)) 0.001)) (define (improve guess x) (/ (+ guess (/ x guess))) 2))
このアルゴリズムを TeX で実装したいわけですが、 TeX にはそのものずばりな実数値がありません。ただ、TeX本来の機能である自動組版を制御するために、値が実数になる「寸法(dimension)」というデータ型は用意されています。このデータ型上にも、整数値のときと同じ演算命令(\advance
、\multiply
、\divide
)が制限付きながら用意されているので、そこに上記のアルゴリズムの x や y を格納すれば原理的にはベキ乗根が求められるだろう、という算段です。
ここで厄介なのが、寸法上の演算の制限です。寸法の値を自由に掛け算したり割り算したりすることはできません。寸法に対して可能な乗算と除算は、具体的には以下のとおり。
- 寸法は、整数倍したり、整数で割ったりすることはできる。
- 寸法の前に、何か数字っぽいものを書くと、その数字の値が寸法の値に掛けられる。
上記の 1. は、寸法の値に実数を掛けたり割ったりすることは基本的にできない、つまり、実数どうしの乗算と除算が寸法型のままではできないということです。
上記の 2. の手段を使えば、寸法に実数を掛けることはできます。ここでちょっと実際の TeX コードを見てみましょう。寸法データ型を用意して「3.14 ポイント」を格納し、その寸法を 1.5倍するには、例えばこう書きます。
\newdimen\x % 新しい寸法データ\xを用意 \x=3.14 pt % 寸法\xに単位付きの実数を格納 \x=1.5\x % 寸法\xを1.5倍
寸法の単位はいくつかありますが、ここではポイント(pt)を使っています。2行めでポイントの単位「pt」を省略して 3.14 という実数値を寸法に代入しようとしてはいけません(TeX に苦情を言われます)。3行めの結果、 \x
には「4.71 ポイント」に相当する値が入ります。
ここで、実数に実数を掛けようとして次のように書いたらどうなるか考えてみましょう。
\newdimen\x % 新しい寸法データ\xを用意 \newdimen\y % 新しい寸法データ\yを用意 \x=3.14 pt % 寸法\xに単位付きの実数を格納 \y=1.5 pt % 寸法\yに単位付きの実数を格納 \x=\number\y\x % 寸法\xを1.5倍?
最後の行に出てくる \number
は、直後のデータ型の数字表現を返すコマンドだと考えてください。上記を TeX に渡すと「Dimension too large.」というエラーになり、 \x
は 16383.99998pt
という寸法になるはずです。これは、 TeX の寸法の上限(をポイント単位で表したもの)です。
何が起こったのでしょう。\number\y
は 1.5 ではなく、「ポイントで表すには "too large" だ」と TeX に言われてしまう何か巨大な寸法であり、TeX はとりあえずの処置としてポイント表示で最大な数字を \x
に入れたのです。ちなみにその巨大な寸法とは、 1.5 ポイントを sp という単位で表した数字に 3.14 ポイントを掛けた寸法であり、308674.56pt
です。実をいうと、TeX は内部では sp という単位の整数倍で寸法を扱っています。だから、寸法で実数が扱えるというのも実はまやかしで、ポイントくらい大雑把な単位で見れば小数表示になる寸法を実数とみなして扱える、ということだったりします。
というわけで、寸法そのものは単位付きの値なので、その単位で表した場合の実数値を示す数字として使うには、何とかして単位の部分をひっぺがす必要があります。そのための仕組みがこちらの \strippt
です。
\newdimen\zero \zero=0pt \begingroup \catcode`P=12 \catcode`T=12 \lowercase{ \def\x{\def\rempt##1.##2PT{##1\ifnum##2>\zero.##2\fi}}} \expandafter\endgroup\x \def\strippt{\expandafter\rempt\the}
何を言ってるのか分からないと思うが、私もこんなのゼロから書けるわけありません。これは LaTeX のコアで定義されているマクロをほぼそのまま拝借してきたものです。 \expandafter
のとこだけ説明しておくと、ポイントを表す「pt」の各文字をちょっと特殊な文字とみなす閉じた環境で「なんとか.なんとかpt」というトークン列にマッチさせて \rempt
というマクロを定義したいのだけど、それを外から使えるようにするために環境を閉じる \endgroup
の展開を遅らせているのが最初の \expandafter
で、その\rempt
を「なんとか.なんとかpt」という形の引数に対して使うべく \the
で先に展開するために配置されているのが 2つめの \expandafter
です。
以上の下準備でようやく、ニュートン法を実装するのに必要なベキ乗のためのマクロ \square
が定義できるようになりました。
\newdimen\tempdimen \tempdimen=\zero \def\square#1{\tempdimen=#1% #1=\strippt \tempdimen \tempdimen \tempdimen=\zero}
この \square
を使ってニュートン法のアルゴリズム全体を書き下してみます。
\newdimen\prevguess \newdimen\val \newdimen\guess \def\sqrtiter{% \prevguess=\guess \square\prevguess \advance\prevguess by -\val % 推測値のベキ乗と元の値との差 \ifdim\zero>\prevguess % 差の絶対値 \prevguess=-\prevguess\fi \ifdim\prevguess<0.001pt % 差が十分小さかったら、 \let\next=\relax % おしまい。 \else % 十分小さくなかったら、 \improve\guess\val % 推測値を改善して、 \let\next=\sqrtiter % 再帰。 \fi\next}
上記でまだ未定義なのは、ニュートン法のアルゴリズムで推測値を改善させる部分を担うマクロ \improve
です。次はこれを実装します。\improve
には「実数÷実数」の演算がでてきますが、寸法の値を使った除算には直接の手段はなさそうなので、やや面倒な工夫が必要です。全体はこんな定義になりました。
\newcount\tempcountA \newcount\tempcountB \newdimen\tempdimenA \newdimen\tempdimenB \def\improve#1#2{% \tempcountA=#2% % いったん割られる数を整数にして、 \multiply\tempcountA by 1000% % 十分な倍率で大きくしておく。 \tempcountB=#1% % 割る数も整数にする。 \tempdimenB=#1% \divide\tempcountA by \tempcountB%% ここで割り算。 \tempdimenA=\tempcountA pt% % 結果をポイント単位で寸法に戻す。 \divide\tempdimenA by 1000% % 倍率を戻す。 \advance\tempdimenA by \tempdimenB% \divide\tempdimenA by 2% #1=\tempdimenA}
\newcount
は、整数を格納できるハコを作ります。そこに「寸法」を代入すると、先に説明したsp単位の整数に変換されて格納されます。整数どうしは \divide
というプリミティブで割り算できるので、これだけでもよさそうに思えますが、 \divide
は剰余を切り捨てて結果を整数に丸めてしまうので、これだけだと結果の誤差が大きくなりすぎます。そこで、割られる数だけ十分な倍率を掛けておいて、割り算して結果を寸法に変換し直したあとで倍率を戻しました。
\improve
の実装に必要な残りの演算は、単純な足し算と整数2による除算なので、特にこれ以上の工夫はいりません。これで寸法 \val
と \guess
にそれぞれベキ乗根を求めたい数と初期推測値を入れておいて \sqrtiter
を起動すると、ベキ乗根の値にポイントの単位がついたものが \guess
に代入されます。あとは、こんなコマンドを定義すれば目標達成です。
\def\sqrtd#1{\val=#1 pt% \guess=1.0pt% \sqrtiter% $\sqrt{\number#1} \approx \strippt \guess$% } You can get \sqrtd{2}, \sqrtd{2.5}, \sqrtd{3} and \sqrtd{3.14} by Newton's method with initial guess $1.0$!
今回書いた TeX スクリプトの全体はこちら。なお、 LaTeX で実数を本気で使いたいなら、こんな面倒なことをしなくても、fpパッケージという便利な浮動小数点数演算のための仕組みがあります。そちらを使いましょう。