golden-luckyの日記

ツイッターより長くなるやつ

抽象データ型を自作する

昨日の記事では「書籍のマクロな構造」について話しました。 このマクロ構造はPandoc構造には組み込まれていません。 そのため、Pandocで書籍を作ろうと思うと、どうしたってPandoc構造にない部分を扱う別の仕組みが必要になります。 素のPandocでは、「書籍のマクロな構造を扱える外部の仕組み」を託す先として、主にLaTeXを利用しています。

裏を返すと、LaTeXは、書籍のマクロな構造を扱える仕組みです。 それなら最初からPandocではなく、LaTeXで本を作ればいいのではないでしょうか?

この反論はもっともです。 実際、本を作るプロは黙ってLaTeXであったり、あるいはInDesignであったり、あるいはFrameMakerであったりを使います。 組版のプロの要求を実現するためには、これらのツールが持つ表現への自由度が必要だからです。

しかし原稿をもらう立場からすると、この高い自由度がかえって仇になるケースのほうが多い。 原稿の肝の部分はPandoc構造くらいに制限されたセマンティクスで書いてもらえるほうがよい。 そのようなわけで当社では、「Pandocで処理をすることを前提としたMarkdown」を、原稿の執筆のための最初の記法として選択することにしました。 Pandoc構造は、最大公約数的な意味で、とてもうまくできていると思います。

なお、LaTeXに関していうともうひとつ難点があって、それは「LaTeX原稿は再利用が容易とはいえない」ことです。 これは、「Markdownのほうが再利用しやすい」という意味ではないので注意してください。 再利用しやすいものがあるとしたら、Markdownのような記法ではなく、そのセマンティックな構造を直接表した抽象データ型のほうです。 Pandocが多様な形式のドキュメントに対応できているのも、この抽象データ型の再利用の容易さの表れだといえるでしょう。

そんなわけで、「これから書く」ものに関してはPandoc構造を前提とした記法でいいと思います。 しかし、世の中には、「すでに書かれたものでPandoc構造では不足がある」ような原稿も当然あります。 標準的な処理系が利用できるLaTeXなどの原稿であればいいんですが、そうではなかったり、そもそも原稿の記法が未知であるようなケースもあります。 そうした原稿を手にした場合に、どうやってそれを手懐けるかというのが今日の話です。

結論からいうと、Pandocが採用しているアプローチ、つまり「記法から代数的データ型を読み取って、それを目的に合わせて出力する」という方針で挑みます。 具体的には、次の3つの仕組みを実装します。

  • その記法が表す構造を押し込めるのに都合がよさそうな抽象データ型の定義
  • それに合わせて記法から構造を読み取るReaderの実装
  • 抽象データ型に対する出力を定義したWriterの実装

Pandocの場合には、最初に意識されていた記法が「Markdownのそれ」だったといえるでしょう。 その実装であるPandoc構造よりもリッチな構造を読み取れそうな記法で書かれた原稿があったなら、その原稿を眺めながら自分で代数的データ型とそれをターゲットにしたReaderを実装し、自分が扱いやすいLaTeXやHTMLのような記法へと変換するWriterを書く、すなわち、自分専用のPandocを実装すればいい、というわけです。

未知の記法に向き合う

処理系が手に入らない未知の記法で書かれた原稿を手にする機会がある人は少ないと思うので、人口的ですが説明のための小さな例を用意します。

\chapter{Lorem Ipsum}

The quick \xdefn{brown fox}\emph{brown fox} jumps over the lazy dog (\ref{fig:fox}).

\bottomfloat \float{figure}
\include{fig1}
\caption{The fox}
\tag{fig:fox}
\endfloat

\section{Dolor sit amet}

Our \mono{etaoin shrdlu}.

\sidebar{consectetur adipiscing elit}
Ut enim ad minim veniam, quis nostrud exercitation.

Ullamco laboris nisi ut aliquip ex ea commodo consequat.
\endsidebar

\section{Excepteur sint occaecat cupidatat non proident, \titlenl; sunt in culpa qui officia}

\list{bullet}
\item Li Europan lingues

\item Lorem Ipsum
\endlist

これ、LaTeXに見えるかもしれませんが、LaTeXではありません。 ただ、TeXのアプリケーションではあります。 TeXというのは、いわば自分自身のオリジナルなドキュメントシステムを作るためのフレームワークで、その実装のひとつがLaTeXであり、LaTeX以外にもさまざまなフレームワークが存在します。 上記は、そのような「どこかのオリジナルなTeXベースのドキュメントシステム」の原稿のつもりです。 (なお、ConTeXtというLaTeXとは別のフレームワークかな、と思った人もいるかもしれませんが、それも違います。)

ここで選択肢は大きく2つあるでしょう。

  1. このTeXアプリケーションをTeX言語で実装する
  2. この原稿の記法をLaTeXに書き換える

具体的な仕様が公開されているTeXアプリケーションであれば、1もありえると思います。しかし、その望みが薄い場合、2のほうが無難でしょう。 とはいえ、この記法をどうやってLaTeXに書き換えるかは、それほど自明な問題ではありません。

ちなみに、この原稿に見えるTeXっぽいけどLaTeXにはないもの、たとえば\sidebarとか\list{bullet}TeXLaTeXの命令として定義してlatexコマンドで実行するという方針は、最悪です。 上記くらい単純だとそれも可能かもしれませんが、未定義のコントロールシーケンスっぽいやつを\defとかでアドホックに定義しても、全体の整合性はまずとれません。エラーを回避しながら何とかPDFにできたとしても、まともに組版できるとは限りません。

これはLaTeXに書き換えるという方針を採用する場合でも同じです。 たとえば、エディタの正規表現で原稿中のコントロールシーケンスっぽいやつをLaTeXのそれっぽいコマンドに変換していくだけでは、事態を悪化させる可能性が高いです。

そこで、Pandocのアプローチに倣って、この未知の記法から構造を取り出すことにします。 構造さえ取れれば、そこから先の工程をある程度は形式的に進められます。 何回か前の記事でPandocのアプローチのことを「ライトウェイト構造化文書」と表現しましたが、そこで含意していたのは、Pandocのアプローチにはこういう性質があるからだったのでした。

抽象データ型の定義

上記の記法を眺めつつ、必要そうなデータ型をHaskellで定義しましょう。 なんどかやっていると勘所がつかめてきます。 ブロックとインラインに分けるみたいな常套手段はあるんですが、結局は勘と経験だと思います。

この例の場合は、たぶんこんな感じに落ち着くと思います。

data MyBlock = Para [MyInline]
             | Header Int [MyInline]
             | FloatElem FloatType FloatPosition [MyBlock]
               Tag Caption [OuterResource]
             | ListItems ListType [([MyInline], [MyBlock])]
             | Sidebar [MyInline] [MyInline]
             | MyEmpty

data MyInline = TextString DT.Text
              | EmphString [MyInline]
              | TtString [MyInline]
              | Ref RefType Tag
              | Label Tag
              | Command DT.Text
              | CaptionCmd Caption

一発で決まることはまずないので、とりあえずデータ型を定義してみる→ReaderとWriterを作って結果と記法を見比べる→定義を追加したり修正したりする、の繰り返しになります。 というわけで、ReaderとWriterを作る話に移りましょう。

ReaderとWriterを作る

Readerは、ここまでの記事でなんどか登場しているParsecで作ります。 Parsecにおける「パーサ」とは、「局所的なパターンを見つけ、必要なデータ型としてreturnする機能をもった関数」のことです。 そういうパーサをいくつか組み合わせることで多機能なパーサを作り、「テキスト全体から構造を見つけてきてデータ型として吐き出す」というのがParsecのノリです。

たとえば、上記のうちブロック要素の構造を見つけてきてMyBlockを吐き出すようなパーサ(関数)は、こんな感じに複数の単機能のパーサの選択として定義できるでしょう。

myBlock :: Parser MyBlock
myBlock = choice
          [ try emptyLine
          , try myInclude
          , try myHeader
          , try myFloat
          , try myList
          , try mySidebar
          , myPara
          ]

emptyLine とか myPara みたいなのが単機能のパーサです。 先に定義した代数的データ型の各部に、各関数がそのまま対応していることが見て取れると思います。 このように代数的データ型との直観的な対応でパーサを定義できるのがParsec(というかパーサコンビネータ全般)の便利なところだと思います。

単機能のパーサは、原稿で記法がどう使われてるかを見て、それをそのまま定義として書き下します。 詳細は省きますが、 mySidebar であれば、「"\sidebar"という文字列に続いて波カッコで括られた文字列があり、そのあとにブロック要素が "\\endsidebar" という文字列が出てくるまで続く」ようなパターンの部分を見つけ出してきて、それを「Sidebar 型として return する」ような関数として定義する、という感じです。

mySidebar :: Parser MyBlock
mySidebar = do
  title <- string "\\sidebar" *> bracedMyInline <* spaces
  contents <- manyTill
              myBlock
              (try $ string "\\endsidebar")
  return $ Sidebar title contents

bracedMyInline = 
  string "{" *> manyTill 
    (try myInline <|> (TextString "\n" <$ (many (string " ") >> newline)))
    (try $ string "}")

Parsec、まじで強いので、テキストを処理するお仕事の人はこのためだけにHaskellを勉強しても損はしないと思います。

Writerを作る

WriterはReaderに比べると簡単で、構造に対する show メソッドを「LaTeXのソースを出力するもの」として定義してあげるだけです。

writeLatex :: [MyBlock] -> DT.Text
writeLatex myAST = DT.concat $ concatMap blockToText myAST

blockToText :: MyBlock -> [DT.Text]
blockToText (Para is) = "\n" : (map inlineToText is)
〈その他いろいろなブロック要素の出力方法をパターンマッチで定義〉

inlineToText :: MyInline -> DT.Text
inlineToText (TextString txt) = txt
〈その他いろいろなブロック要素の出力方法をパターンマッチで定義〉

実際には細かいところはいろいろ考えて実装しないといけないのですが、ノリとしてはこんな感じになるはずです。 時間と体力の都合により全体の実装は省略します。

まとめ

冒頭に提示した例は、実際に過去に自分が出会った「処理系が手に入らないTeXアプリケーション」の原稿を基にしています。 そのTeXアプリケーションはZzTeXというもので、英語圏の専門書の出版社でときどき採用されているようです。 調べたら、TUGboatに紹介記事がありました。

このときZzTeXの原稿をLaTeXへ変換するのに書いたプログラムでは、抽象データ型の定義だけで、最終的に全部で100行くらいの規模になりました。 今回の記事で例としてでっちあげた程度のドキュメントであれば、Pandoc構造に合わせてReaderを書くので十分かもしれません(足りない部分はDivとSpanでやる)。 しかし、これくらいの規模の構造を掘り起こさないといけない場合には、今回のような戦略で変換器を書くのがいちばん近道に思えます。

なお、この記事で「TeXアプリケーション」と呼んだものは、「TeXのフォーマット」とか「TeXのマクロパッケージ」とかって呼ぶほうが正確です。 マクロとかフォーマットというと、LaTeXをなんとなく使ったことがある人が知っているような気がする別のものが想起されやすいので、ここではTeX言語によるアプリケーションと表現することにしました。

明日はEPUBライターの話を予定していて、ぜんぜん話が変わるようですが、またHaskellを使います。