XMLの論考: DOM、SAX、およびXSLTの限界を超える

XML用のHaXml関数型プログラミング

XMLデータを処理するとき、DOMやSAXやXSLTではなく、Haskellの使用を考慮してみてください。HaXmlライブラリーは、XML文書を関数型言語Haskell固有の再帰的データ構造で表現します。HaXmlには、こうした「データ構造化された」XML文書を処理するための、高次 (higher order) 関数が提供されています。DOM、SAX、XSLTといったよく知られている技法と比べて、HaXml技法は多くの点ではるかに洗練され、しかもコンパクトで強力です。この技法をサンプル・コードによって示します。

David Mertz, Ph.D (mertz@gnosis.cx), Author, Gnosis Software, Inc.

Photo of David MertzDavid Mertz氏は多くの分野で活躍しています。ソフトウェア開発や、それについて著述もしています。その他、学術政策理念について分野を問わず、関係する雑誌に記事も書いています。かなり以前には、超限集合論、ロジック、モデル理論などを研究していました。その後、労働組合組織者として活動していました。そして、David Mertz氏自身は人生の半ばにもまだ達していないと思っているので、これから何かほかの仕事をするかもしれません。



2001年 10月 01日

XML文書を処理する技法のうち最も普及したものとしてはDOM、SAX、およびXSLTがあります。しかし不便なことに、これらの技法の間には共通の原理が欠けています。どんなXML処理作業でも、これらの主要な技法のどれか1つを使って行うことはできます。しかし、各々の技法の最も優れた部分を同時に利用しようとすると、とても単純とは言えない問題にぶつかります。おそらく、多種多様な技法やツールを使った細かな変換がたくさん混在する、非常に複雑なアプリケーションを使用せざるを得ないでしょう。

DOMは、XMLのすべてをオブジェクト指向プログラミング (OOP) の枠組みにはめ込みます。その際、どのプログラム言語よりも高度な抽象化を行います。DOMは言語に依存せず、その "文書オブジェクト・モデル (Document Object Model)" は、多数の汎用プログラム言語のライブラリーとして提供されています。DOMはすべてのメソッドと引数を指定しますから、ある意味でDOMは言語です。ライブラリーの基礎となる汎用言語は単なる接着剤にすぎません。DOMが何となく人工的なので前回のコラムでご紹介した私のユーティリティーxml_objectifyでは、XML文書を (Pythonの形で) 本来のOOPにより近い感じのオブジェクトに変換する1つの方法を提供しました。

SAXは言語非依存性の点でDOMと似ていますが、DOMにおけるOOPの枠組みの代わりに、イベント・ドリブンの手続き型モデルを使用しています。SAXには、XML文書をストリームとして処理できる便利な機能があります。それぞれの要素や内容が検出されるたびに処理が行われるのです。しかし、イベント・ドリブンの原理ゆえに、SAXにはXML文書が表すデータ構造の概念がまったく欠如しているという欠点があります。確かにこうした構造をアプリケーションで構築することはできますが、(親子関係のような) 基本的な概念でさえ、SAXライブラリーの基礎となっているプログラム言語のボキャブラリーで表さなければなりません。SAXそのものは、XMLの持ち味をほとんど無視しています。

最後にXSLTは、ある意味ではXMLの構造に最もよくマッチした普及している技法です。よくマッチしている証拠に、XSL文書それ自体がXML文書のインスタンスです。XSLTは特殊な目的の関数型 プログラミング言語で、XML文書を他のもの (とくに他のXML文書に変換できますが、それだけに限られてはいません) に変換する方法を指定できます。XSLTはかなり冗長であるのに加えて、表現力も限られています。処理内容はかなり明確に (しかも手続き型ではなく関数型として) 表現できますが、しばしば遭遇する問題は、XSLTで表現できない処理が山ほどあるということです。

簡単で標準的な作業

ここで、普及している技法の弱点を露呈するきわめて現実的なシナリオを示しましょう。通常、XML文書から出力への変換を記述する最も直接的な方法はXSLTです。たとえば、XML文書のHTML表記を作成するような場合が考えられます。そこで、リスト1のような、XMLバージョンのI Ching について見てください。

リスト1. XMLバージョンのI Ching
<?xml version="1.0"?>
<IChing>
    <title>Some Hexagrams from the I Ching</title>
    <hexagram>
        <number>1</number>
        <name>Ch'ien / The Creative</name>
        <judgement>
            The Creative works sublime success,
            Furthering through perseverance.
        </judgement>
    </hexagram>
    <hexagram>
        <number>2</number>
        <name>K'un / The Receptive</name>
        <judgement>
            The Receptive brings about sublime success,
            Furthering through the perseverance of a mare.
        </judgement>
    </hexagram>
    <hexagram>
        <number>3</number>
        <name>Chun / Difficulty at the Beginning</name>
        <judgement>
            Difficulty at the Beginning works supreme success,
            Furthering through perseverance.
        </judgement>
    </hexagram>
</IChing>

この情報をHTML表として表示するには、たとえばリスト2のようなXSLT命令を使用します。

リスト2. I Ching HTML表を作成するためのXSLT命令
    <xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns="http://www.w3.org/TR/xhtml1/strict">
    <xsl:output method="html" indent="yes" encoding="UTF-8"/>
    <xsl:template match="IChing">
      <html>
        <head><title><xsl:value-of select="title"/></title></head>
        <body><table border="1"><xsl:apply-templates/></table></body>
      </html>
    </xsl:template>
    <xsl:template match="hexagram">
        <tr><xsl:apply-templates/></tr>
    </xsl:template>
    <xsl:template match="number">
        <td><xsl:apply-templates/></td>
    </xsl:template>
    <xsl:template match="name">
        <td><xsl:apply-templates/></td>
    </xsl:template>
    <xsl:template match="judgement">
        <td><xsl:apply-templates/></td>
    </xsl:template>
    <xsl:template match="*"></xsl:template>
</xsl:stylesheet>

リスト2のXSLTは単純で直接的に見えます。それぞれの要素がどのように形式設定されるかを示すテンプレートをただ作成するだけです。これほど簡単なことはありません。しかし、出力用に何らかの形でフィルター掛けや加工処理をしようとすると、たちまち問題に突き当たります。こうした機能は、XSLTの数少ない比較機能には含まれません。たとえば、(数秘術に凝っている人が) 偶数の六線星形だけ、あるいは素数のものだけを表示したいと思うかもしれません。XSLTでは、残念ながら、こんな簡単なこともできません。

ここで、代わりにDOMやSAXを利用してこうした作業を行うこともできます。確かにこれは可能ですが、先のXSLT変換で行った作業をすべて捨て去らなければなりません。DOMやSAXはまったく別のモデルです。XSLTと共通しているコードや概念は、事実上まったくありません。私のおもちゃのようなスタイル・シートのように簡単なものであれば、たいして問題になりません。しかし大規模な実動レベルの変換では、失うものが大きいでしょう。

さらにSAXとDOMのどちらも、出力用モデルとして洗練されていませんし、保守が容易でもありません。すでに説明した簡単な (フィルターされた) HTML表を出力するためには、アプリケーション・コードの中にprint ステートメントやprintf() 関数 (その他、普及している言語の適切な命令) を散りばめる必要があります。こうした出力ステートメントそのものは、要素型その他の条件を検査する条件ブロックの中に埋め込まれます。そのようなコードの中では、出力のフローを一目瞭然に示したり、それぞれのprint "<tr>" に対応するprint "</tr>" が検出されることを確認したりする方法はありません。出力HTMLまたはXMLが整形式であることを、命令コードやOOPコードを使って確かめる方法はありません (妥当性検査が不可能なことは言うまでもありません)。

DOMの冒頭に "writer" (書き込み) メソッドを作成することもできます。この例として一般的なのは、Javaのorg.w3c.dom.html パッケージ、およびPythonのxml.dom ノード・メソッド.writexml() です。これらのメソッドはDOMツリーを入力とし、適切なHTMLを出力します (整形式であることも保証されます)。ただし、私がこれまで観察した範囲で言うと、いくつかの構成オプションに限られた「適切な」HTML、というのが私の実感です。(XSLTやHaXmlのように) どんなHTMLでも出力できるほど十分ではありません。いずれにしても、出力の前に、DOMツリーを手続き型操作で処理するという過程が残っています。これは関数型に比べて不明瞭で、バグが存在する可能性も高いのです。


統合された解決策

(XSLTのように) 出力を宣言型で表せて、しかも (DOMやSAXの基礎となっている実装言語のように) フィルターや加工処理を任意に含めることのできるシステムであれば、XML変換用として理想的です。そのような処理の際、出力が整形式になっていることを (そして妥当性さえも) 確認する必要がない方が望ましいでしょう。また、コンパクトで直接的な構文であれば、さらに便利でしょう。

HaXmlはこれらの要求をすべて満たします。実際、HaXmlの能力はこうした要求を満たすことのみにとどまりません。Haskellのような関数型プログラミング言語で可能な高次の組み合わせ関数を活用すれば、複数のフィルターを任意に組み合わせて出力を指定できます。XSLTでは、それぞれの<xsl:template> が入力と出力の間の一種のフィルターのような働きをします。しかし、XSLファイルによって作成される事実上唯一のフィルターの組み合わせは、すべてのフィルターの和集合です。対照的にHaXmlでは、出力するそれぞれの要素ごとに一連のフィルターをかなりきめ細かに指定することができます。実際、XSLTでXPathを使用すればこれとほとんど同じ機能を実現できますが、HaXmlの方がずっと軽量で、厳密に言ってより強力です。

多数の組み合わせを実現できることに加えて、HaXmlではフィルターリングの一部としてHaskell言語による任意の加工処理を含めることができます。もう1つ便利な機能は、より理路整然としたブロックで出力を指定できることです。これによって、出力条件とフィルター条件を読みやすい形で織り混ぜることができます。その結果、リスト3に示されているようにXSLTよりもずっとコンパクトになりますし、コードを読みにくくする原因となる句読点記号もわずかしかありません。

リスト3. I Ching HTML表を出力するHaXmlプログラム
module Main where
import XmlLib
-- Concise XSLT-like specification of output
main = processXmlWith (hexagrams `o` tag "IChing")
hexagrams =
    html [
      hhead [htitle [keep /> tag "title" /> txt] ],
      hbody [htableBorder [rows `o` children `with` tag "hexagram"] ]
    ]
htableBorder = mkElemAttr "TABLE" [("BORDER",("1"!))]
rows f =
    let
      num = keep /> tag "number" /> txt
      nam = keep /> tag "name" /> txt
      jdg = keep /> tag "judgement" /> txt
    in
      if (condition (num f) (nam f) (jdg f))
      then hrow [hcol [num], hcol [nam], hcol [jdg]] f
      else []
condition num nam jdg = isPrime (makeInt num)
-- Supporting computations for rows condition
makeInt = stringToInt . unwrap      -- Turn a Content into an Integer
unwrap [(CString b c)] = c          -- Turn a Content into a String
stringToInt = revToInteger.reverse  -- Turn a String into an Integer
    where
    revToInteger = toInteger . revToInt
    revToInt []  = 0
    revToInt (d:ds) = digitToInt d + (10*(revToInt ds))
-- ordered search of Sieve of Eratosthenes
isPrime = ordSearch (sieve [2..])  where
    ordSearch (x:xs) n
        | x < n     = ordSearch xs n
        | x == n    = True
        | otherwise = False
    sieve (x:xs) = x : sieve [y | y <- xs, y `mod` x > 0]

XSLTと同様に、一連の定義はどんな順序にすることもできます。コードの最初の20行は出力フォーマットを指定し、いくつかの定義はサポートされている関数ごとに細分化されています (これは、単に見やすくするためです)。Haskellの構文では、等号の左側にくるものが関数であり、定義は等号の右側にきます。where 文節とlet 文節は、他の言語で言うところの「内部関数」のようなものを指定します。それらの第1行は、概念的にXSLT手法にとてもよく似ています。しかしHaXmlバージョンのより進んだ点として、フィルターを必要に応じて随時定義できることがあります。フィルターの1つの例は次のとおりです。

rows `o` children `with` tag "hexagram"

`o` とは、いわば「アイルランド風の書き方」であり、of とも発音されます。rows は私が便宜上定義したフィルターです。rows は標準的なフィルターと同じように自由に使用でき、しかも組み合わせに制限はありません。

プログラムの後半部分には、加工処理を行う関数がいくつか含まれています。1つの優れた例として、6行からなる素数性テストがあります。これは、Haskellの言語としての能力と洗練されていることをよく示しています。プログラムが構造化されているので、関数condition<number><name>、または<judgement> の各要素のContent (内容) に対してどんなテストでも首尾よく実行できるでしょう。Content は特殊なデータ型ですから、まず最初に、Content に含まれるStringをunwrap する必要があります。その後、それをIntegerに変換してテストする (または、他の任意の処理を行う) ことができます。

HaXmlライブラリーにはもっと多彩な機能が含まれています。そして、Haskellに習熟すること自体、手続き型プログラミングやOOPスタイルのプログラミングに慣れているプログラマーにとって、学習の努力が必要になります。しかし、XSLTと同様の機能にのみ焦点を絞るとしても、サンプル・プログラムの前半部分はごく簡単に拡張できるでしょう (私の考えでは、XSLTよりも良好な学習曲線を描きつつ、XSLTと同様の機能を利用できます)。構文がXSLTより読みやすいだけでなく、プログラム後半部分のような操作 (また、さらに多くの操作) を行う方法をただちに学習する必要もありません。


妥当性の検査

上記の例では、XML文書が一般的なツリー構造として扱われました。これは、ほとんどの場合に最も簡単で迅速な方法です。しかしHaXmlには、これとはまったく違うユニークな機能があります。処理対象の文書タイプ用のDTDがすでに存在する場合、Haskell固有の一連のデータ構造をそのDTDから生成できるのです。その後、固有のDTDを利用して操作を行うアプリケーションを作成できます。DTDからHaXmlデータ構造を生成する課程は、いくつかのステップから成ります。最初のステップは、たとえば次のようにして、データ構造をモジュールとして作成 することです。

% DtdToHaskell MyFile.DTD MyFileDTD.hs

データ構造のモジュールが利用できるようになったら、DTDに従うXML文書を作成することができます。通常、そのようなアプリケーションは、少なくともリスト6のような簡単な行を含みます。

リスト6. Custom HaXml app for MyFile.DTD XML documents
import Xml2Haskell (readXml)
import MyFileDTD
[...]

それから、Haskellの提供する高次の技法を駆使して再帰的データ構造を処理することができます。通常、まず最初に、特定の入力XML文書を扱うためにreadXml (XMLの読み取り) を行うでしょう。

ここで読者は「それがどうしたのか」と思われるかもしれませんが、無理もありません。DOMやSAXの手法と何ら変わらないように見えるからです。DOMやSAXではまったく問題なく構造化データを処理し、またDTDに照らして妥当性を検査できます。

実は、目に入ってくるよりもずっと多くのものが潜んでいるのです。他のほとんどのプログラム言語とは異なり、Haskellはデータ構造に関して完全に 型セーフ (type-safe) です。Haskellの場合、妥当でないXML文書を形成するような内部データ構造を作成することは不可能 なのです。それとは対照的に、C/C++、Java、Perl、Smalltalk、Pythonといった言語でできることといえば、入力時または出力時に精神鑑定 (つまり妥当性検査 ) を行って、結果が良いことをただ期待するにすぎません。Eiffelのようなものの場合、"adder" や "deleter" ごとに限定的な制約を追加してDTD妥当性を維持することも可能でしょう (あるいは、相当の努力を払えば、さきに挙げたすべての言語でそうすることは可能でしょう)。しかし、そのためには、各.addSpamTag() メソッドの中でカスタム・プログラミングをする必要があります。しかも、さきに挙げた言語でDTDにできることといえば、アプリケーション・プログラマーのためにカンニング・ペーパーを提供するだけです。

HaXmlでは、DTDからプログラム的に生成されるデータ構造には、すべての妥当性制約が自動的に含まれます。もちろん、ただ制約を施行するだけで、アプリケーション・プログラマーが正しいコードを書けるわけではありません。しかし、少なくとも、無効な文書になるような不適当なコードがコンパイル時に検出されます。もう1つ注意すべき点は、当然のことですが、妥当性を検査するカスタム・アプリケーションを書くには、妥当性検査しないアプリケーションを書くよりも多くのプログラミング作業が必要だと言うことです。それでも、「重要業務」という要件のもとで、HaXmlは厳しい目標に最も早く到達できる近道、しかも最も安全な道となるでしょう。

参考文献

  • Bijan Parsia著の非常に興味深いエッセー、Functional Programming and XML をXML.comでご覧ください。Parsiaは、XMLを処理するうえで、普及しているOOP技法よりも関数型プログラミング・スタイルの方が、一般的に言ってより適していると述べています。そしてHaXmlその他のいくつかのツールについて説明しています。
  • HaXmlに関する詳しい説明が、そもそもの作成者であるMalcolm WallaceとColin Runcimanによって書かれています。Haskell and XML: Generic Combinators or Type-Based Translation をご覧ください。これを読むには、Haskellおよび関数型プログラミングに関する予備知識として今回のコラム以上のレベルが必要とされますが、WallaceおよびRunciman共著のこのペーパーには、今回のコラムで取り上げなかった詳細が数多く含まれています。
  • Haskellを紹介しているHaskell言語Webサイトには、チュートリアル、多数のペーパー、さまざまなコンパイラーとインタープリターがあります。
  • このコラムで取り上げたファイルのZIPアーカイブを、私のアーカイブからダウンロードできます。
  • この記事で説明した変換のサンプル出力は、以下のサイトにあります。
  • XMLの論考の過去のコラムでは、次のようなさまざまなトピックを扱っています。
    • XMLの論考 第1回では、Pythonのxml_pickleオブジェクトを紹介しています。
    • XMLの論考 第2回では、Pythonのxml_objectifyの使い方について説明しています。
    • XMLの論考 第3回では、DocBookを紹介しています。
    • XMLの論考 第4回では、引き続き、DocBookでレガシー文書アーカイブを構築する方法について説明しています。
    • XMLの論考 第5回では、XSLTを介してXML文書をHTMLに変換する方法について説明しています。
    • XMLの論考 第6回では、いくつかのXMLエディターを比較し、特に、テキスト中心の文書に適しているものを取り上げています。
    • XMLの論考 第7回では、DTDとXML Schemaを比較検討し、開発者がどのような場合に、成熟しつつあるW3C XML Schemaに背を向けてDTDにこだわりたくなるのかを示しています。
    • XMLの論考 第8回では、コンピューター科学者たちによって概念化されたデータ・モデル という抽象的な理論が、特定の複数表現データ・フローを開発するために、どのように役立っているのかを説明しています。
    • XMLの論考 第9回では、RDBMSから独立したXML結果セットを生成できる、パブリック・ドメインのsql2dtdおよびsql2xmlユーティリティーについて説明しています。
    • XMLの論考 第10回では、Davidの「魅力的なPython」第15回のコラムで紹介された汎用全文検索システムを拡張して、XML特有の検索および索引付け機能を組み込みます。また、どのようにindexerがXMLの階層ノード構造を活用できるかを説明します。
    • XMLの論考 第11回では、このシリーズの第1回で紹介したモジュールに再び立ち戻ります。
    • XMLの論考 第12回では、SQLステートメントを生成するパブリック・ドメインのユーティリティーについて説明します。そのSQLステートメントを使えば、XML文書を開始するときに、一貫性のある、反転可能な方法でデータベースを作成して、それにデータを読み込むことができます。
    • XMLの論考 第13回では、XML文書のいくつかの圧縮方法を調べます。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=XML
ArticleID=239901
ArticleTitle=XMLの論考: DOM、SAX、およびXSLTの限界を超える
publish-date=10012001