-*- Menu -*-
-*- ReasonSet -*-

Document Meta Formatの価値とPureDoc

以前ConoHaのイベントであるtech OYAJIでも取り上げさせていただいたテーマですが、DSL、そしてドキュメントメタフォーマットについて述べたいと思います。

Document Meta Formatとは、そのまま取り扱うのではなく、何らかの形式に変換して使うことを意図した文書の記述形式です。例えばHTMLに変換することを前提としたMarkdown、HTMLやTeX、roffに変換することを前提としたPOD、またHTMLまたはriに変換するRDocなどがあります。

document meta formatを用いる最大の理由は、変換先フォーマットよりも簡潔に書くことができるためです。Markdownはこの特長が顕著で、「読みやすく、書きやすい」形式で記述することができます。Markdownの生成先はHTMLと決まっていますが、表現可能な範囲はHTMLよりも狭く、貧弱なドキュメント記述言語です。しかし、それは必ずしも欠点とは言えません。だからこそ簡潔な記述を可能にしているとも言えます。

また、元のフォーマットに対して機能を追加する場合もあります。例えば制御構造やループなどで、特にHTMLの場合記述が煩雑になるリストアイテムやオプションなどの繰り返しをより簡潔に書くことができるようにする、という場合もあります。このような構造的な拡張や動的な生成を意図したものとしてはPHPやeRubyなどがあります。これらはこのカテゴリの中でも特に「プログラム」であり、チューリング完全なプログラムを記述することができます。

また、document meta formatを介することにより複数のフォーマットを生成できる、というメリットもあります。例えばPODならばHTML/TeX/manをはじめPageMaker形式など様々なフォーマットを生成することができます。これは一種の抽象化機能であるとも言えます。

tech OYAJIでも紹介させていただいた https://github.com/reasonset/reasondoc は私が制作しているドキュメントメタフォーマット及びトランスレータです。このformatは完全にRubyを使用したRuby DSLであり、その機能はRubyの全機能を含むものです。Ruby 2.0向けのプログラムですが、Ruby 1.9でも動作するのではないかと思います。

PureDocは次の目的を持って開発されました。

  • HTMLで書くのは冗長である
  • ドキュメント中にロジックを含めたい(動的要素)
  • 印刷用ドキュメントの生成を可能にしたい
  • HTMLの仕様にない意味表現を加えたい

ということです。例えば「現在の日付」「辞書の使用」「if」などの機能を使いたいためにプログラマブルなものにし、かつ簡潔な記述を求めたということでもあります。また、テンプレートへの展開を考えると、eRubyテンプレートを使うためにドキュメントがRubyで生成されているのであればそのRubyスクリプト上からerbライブラリを使えばそのドキュメント内で生成したオブジェクトに直接アクセスできるため、テンプレートでより多くの情報にアクセスできます。そのため、Ruby DSLを用いたドキュメントメタフォーマットを自作しました。

そして自作する最大の理由が最後のHTMLを拡張するということです。HTMLにはないpure:codeblockpure:noteといった要素をXHTMLのXML拡張を用いて表現します。もちろん、HTMLで直接書くことはできますが、既存のその他のdocument meta formatは用いることができないということになります。この時点で自作せざるをえないのですが、「自分の要求に合ったフォーマットが存在しないので作ってしまおう」というのがその発想です。なお、トランスレータはそのようなXHTML拡張だけでなくclassやinline styleを使った生成にも対応しており、当面のアクセシビリティを確保します。

PureDocトランスレータはそれ自身がRubyで書かれており、大量のcode generationによって作られています。その大部分はメタメソッドの定義であり、大半の要素はそのメタメソッドを呼び出すことで実装されています。PureDocドキュメントはそのトランスレータをロードし、トランスレータのオブジェクトを生成することで利用することができますが、実際にはラッププログラムがあり、そのラッププログラムが

  1. トランスレータをロード
  2. トランスレータオブジェクトを生成
  3. ドキュメントをロード

とすることでマナーに則って書くだけで生成できるようになります。ロードするトランスレータライブラリ(トランスレータオブジェクトのクラス)を変えることで生成内容を変化させることができます。

トランスレータは自動で出力しますが、ドキュメントをロードしたトランスレータオブジェクトをさらに触りたい場合は、トランスレータライブラリを手動でロードするRubyスクリプトを書くことで利用することができ、また単なるPureDocドキュメントだけでなく、PUreDocの記法を用いることができるさらなるフォーマットの生成にも利用できます。例えば、このサイトのプロフィールはRubyスクリプトになっており、実行することでプロフィールを生成することができます。

########################################### # NOTICE # ########################################### # This program use $profile_template # # and $puredoc_template # # environment paramaters. # ########################################### require 'date' require 'erb' ########################################### # DEFINITION CODE SECTION # ########################################### $: << "#{ENV['HOME']}/.yek/rubylib" require "htcpdoc" # Use PureDoc::HTMLClass # Over all profile container. # This Object has some Cats. class PF < Array def[](index) self.detect {|i| i.id == index } # serch index name with #[] end end # Each category. # This object has C = Struct.new(:id, :title, :elements) { def ti self.title[$lang] end def each self.elements.each {|title, summery, detail=nil| yield(title[$lang], summery, detail) } end } class Profile < PureDoc::HTMLWithClass def is(&block) @prof = self.instance_eval(&block) end attr_reader :prof end DOC = profile = Profile.new ########################################### # SETTING SECTION # ########################################### #Language index $lang = 0 #Metadata definition DOC.meta["title"] = "Profile" ########################################### # PROFILE # ########################################### profile.is { @pr = PF[ ############## Basic ############## C[:basic, ["Basic personal datas", "基本情報"], [ [ ["HN", "ハンドルネーム"], li { %w<柊美亜紀(とうみあき) ○☆温泉☆○ YEK UsK MAY> }], [ ["Real Name", "本名"], "正木悠介(まさきゆうすけ)"], ... ] # End of 3rd argument. ]# END OF Favorite ] } ########################################### # OUTPUTTING # ########################################### DOC.is { @body = ERB.new(File.read(ENV["profile_template"]), nil, "%").result(binding) } ERB.new(File.read(ENV["puredoc_template"]),nil, "%<>").run

このような柔軟性と強力さを実現するのが内部DSLの力です。

内部DSLとは、あるプログラミング言語の機能を拡張する(関数やメソッドやマクロを定義する)ことで生み出される別の小さな言語です。PureDocの場合、母体は完全にRubyであり、完全なRubyコードを書くことができます。一方で、Rubyとは別に「PureDocの書式」というものがあり、PureDoc自体がひとつの言語ともなっています。

対する外部DSLはsedawkのようにプログラムが独自の表記形式、言語を持っているものです。内部DSLのほうが実装が簡単で(実装に元となるプログラミング言語の機能を使えるため)、さらに機能も強力(その気になれば元となるプログラミング言語の全機能を使うことができる)という特長があります。一方欠点としては、元となる言語の文法に制約される、ということがあります。RubyはDSL向きの言語であるとされています。実際にカッコの省略やブロックなどかなり柔軟かつ「DSLっぽい表記」が可能ですが、それでももっと色々と記号を使いたい、文字列をクォートしたくない、辞書定義を後ろで行いたいなど不満はあります。とはいえ、そのために外部DSLで独自実装するかというと、Rubyに寄生することで得られるメリットのほうがはるかに大きいため、今のような形に落ち着いています。

ちなみに、昔はPureDocはZshの内部DSLになっていました。展開タイミングやスペース、複数行の取り扱いといった事情からZshよりもRubyが適当ということになり、Rubyに変更されています。

RubyコミュニティではDSLについては色々と議論があるようで、自分がしたいことを簡潔に書くことができるDSL形式というのはプログラミングを容易かつ簡潔にするという観点からも注目されており、RubyにおけるDSL的アプローチもまた注目を集めており、その分議論を呼んでいる、ということのようです。

PureDocの場合、プログラム的なDSLではなく、あくまで文書であり、文書を内部DSLで実装するというのはかなり珍しいケースであると認識しています。別の言い方をすれば、少なくともドキュメントフォーマットとしてはPureDocはかなりロジカルである、という見方をすることもできるでしょう。実際にPureDocは単なるドキュメントというだけでなくサイトの自動構築の一端を担っている部分がありますので(例え使われなくても自分が使うために有益な開発である)、そうした「プログラム的な文書」の性質を帯びるのは必然だという見方もできます。また、内部DSLはそのように既存のプログラミング言語を自分の要求に合わせて特化した仕様にするものである、という見方もでき、それが容易に可能であるというのが魅力であるとも言えます。自分が欲しい感じの記述法がないから作ってしまえ、という行為自体が低コストです。

なお、PureDocでも検討されたZsh DSLですが、もともとシェルスクリプトはDSL的であるため、関数などを定義してDSLを作るのは非常に容易です。シェルスクリプト自体がシェル上で作業を一言で表すためのDSLの語彙であるという言い方もできます。まずはシェルDSLを試してみるのもひとつかもしれません。

Since: 2014-11-10 18:20:38 +0900
Last update: 2014-11-19 13:32:02 +0900
About