魅力的なPython: 宣言型ミニ言語の作成

命令ではなくアサーションとしてのプログラミング

Pythonのオブジェクト指向機能と透過的なイントロスペクション機能を利用すれば、いろいろなプログラミングに利用可能な宣言型ミニ言語 を簡単に作成することができます。今回Davidが解説するのは、Pythonを使って他の特殊な言語を解釈したり変換するという手法ではなく (もちろん、それも可能なのですが)、Pythonコードそのものを一連の宣言型の要素に制限することで有効な使い方ができるという点です。宣言型の手法を用いれば、裏方のフレームワークにたくさんの仕事をやってもらうことで、アプリケーションの仕様を簡潔かつ明瞭に記述できることを紹介します。

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

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



2003年 2月 27日

プログラマーは、プログラミングと言えば、命令型 (imperative) のスタイルや手法によってアプリケーションを記述することを考えます。Pythonその他のオブジェクト指向言語を含め、ごく一般的に使用されている汎用型のプログラミング言語は、スタイルとしては、だいたいが命令型です。他方、関数型言語 (functional language) でも論理型言語 (logic language) でも、あるいは汎用言語でも専用言語でも、宣言型 (declarative) のスタイルをとっているプログラミング言語も数多く存在します。

カテゴリー別に言語をいくつか挙げてみたいと思います。それらのツールの多くを使ってきた読者も多いことと思いますが、必ずしもそれらの間の分類上の違いについて考えることはなかったのではないでしょうか。Python、C、C++、Java、Perl、Ruby、Smalltalk、Fortran、Basic、xBaseは、どれも、まぎれもなく命令型のプログラミング言語です。この中にはオブジェクト指向のものもありますが、それは単にコードとデータの構成方法の問題であり、基本的なプログラミング・スタイルの問題ではありません。これらの言語では、プログラマーは、一連の命令を実行するよう、プログラムに指令します。変数に何らかのデータを入れ、その変数からデータを読み出し、何らかの条件に合致するまで 1群の命令をループ させ、何か別のことが真なら 何かを行う、という具合です。これらの言語の良いところの一つは、プログラムを馴染みの時系列的なメタファで考えることが簡単だということです。通常の生活は、1つのことを行い、何か選択を行い、また別のことを行う、といったことから成り立っています。その際には、多分ツールもいくつか使用されます。コックやれんが積み職人や自動車の運転手の作業をプログラム化したものを、コンピューターが実行することは容易に想像できます。

Prolog、Mercury、SQL、XSLT、EBNF文法、あるいはいろいろなフォーマットの構成ファイルなどの言語は、いずれも、何かが該当する事例であること、あるいは何らかの制約条件が適用されることを宣言します。関数型言語 (Haskell、ML、Dylan、Ocaml、Scheme) も似てはいますが、プログラミング・オブジェクト間の内部的 (関数的) 関係 (再帰、リストなど) を記述することのほうに重きが置かれています。われわれの通常の生活には、少なくともその叙述的な性質においては、これらの言語のプログラミング構文に直接対比できるものはありません。しかしながら、これらの言語で自然に記述できる問題を扱う場合には、宣言型の記述は、命令型の解法よりも、はるかに簡潔であり、格段に エラーを起こしにくいものとなっています。たとえば、以下のような1組の線型方程式があったとします。

リスト1. 線型方程式系の例
10x + 5y - 7z + 1 = 0
17x + 5y - 10z + 3 = 0
5x - 4y + 3z - 6 = 0

これは、オブジェクト (x、y、z) の間のいくつかの関係を簡潔な形でかなりきれいに記述したものです。現実生活の中でも、いろいろな形でこうした問題に出くわすことがあるかもしれませんが、紙と鉛筆を使って「xを解く」というのは、面倒で瑣末な話であり、間違いも冒しやすいものです。Pythonで手順を記述するのは、デバッグの観点から、おそらくもっと旨くない方法だと思います。

Prologは、論理や数学に近い言語です。Prologでは、真であるとわかっている文を記述するだけで、後は、アプリケーションに帰結を導き出してもらいます。文は、特別な順序で組み立てる必要はなく (線型方程式に順序がないのと同様)、プログラマーやユーザーである皆さんは、どんな手順を踏んで帰結が導き出されているのかについて何も知る必要がありません。以下がその例です。

リスト2. Prologのコード例family.pro
/* Adapted from sample at:
<http://www.engin.umd.umich.edu/CIS/course.des/cis479/prolog/>
This app can answer questions about sisterhood & love, e.g.:
# Is alice a sister of harry?
?-sisterof( alice, harry )
# Which of alice' sisters love wine?
?-sisterof( X, alice ), love( X, wine)
*/
sisterof( X, Y ) :- parents( X, M, F ),
                    female( X ),
                    parents( Y, M, F ).
parents( edward, victoria, albert ).
parents( harry, victoria, albert ).
parents( alice, victoria, albert ).
female( alice ).
loves( harry, wine ).
loves( alice, wine ).

まったく同じだというわけではありませんが、同じような精神のものにEBNF (拡張バッカス・ナウアー記法) による文法の宣言があります。たとえば、以下のような宣言を行ったりします。

リスト3. EBNFの例
word        := alphanums, (wordpunct, alphanums)*, contraction?
alphanums   := [a-zA-Z0-9]+
wordpunct   := [-_]
contraction := "'", ("clock"/"d"/"ll"/"m"/"re"/"s"/"t"/"ve")

これは、単語の検出を行いたい場合に、それをどんなものとして 捉えるのかを簡潔に記述したもので、単語を認識するための一連の命令群は記述されません。正規表現も同様です (事実、正規表現で上の文法生成ルールが記述できます)。

さらに別の宣言の例としては、XML文書の方言 (dialect) を記述するための文書型宣言があります。

リスト4. XML文書の型宣言
<!ELEMENT dissertation (chapter+)>
<!ELEMENT chapter (title, paragraph+)>
<!ELEMENT title (#PCDATA)>
<!ELEMENT paragraph (#PCDATA | figure)+>
<!ELEMENT figure EMPTY>

他の例と同様、DTD言語の場合も、XML文書を認識したり作成するための処理について命令は一切記述されません。XML文書であるとすれば、それがどういう形式となるのかを記述するだけです。宣言型言語には、仮定法が使用されるのです。

インタープリターとしてのPythonと環境としてのPython

Pythonのライブラリーは、まったく違った2通りの方法で宣言型言語を利用することができます。多分、一般的なのは、Python以外の宣言型言語をデータとして扱い、構文解析して処理するという方法です。アプリケーションやライブラリーでは、外部のソースを (内部的に定義された文字列ではあるが、「わけのわからないデータの塊 (blob)」として) 読み込み、何らかの方法でこれら外部の宣言に対応させて一連の命令手順を生み出し、実行することが可能です。要するに、この種のライブラリーは、「データ駆動型」システムになっているというわけです。宣言型言語と、Pythonアプリケーションでその宣言を実行したり利用するために行うこととの間には、概念的でカテゴリー的なギャップがあります。実際、まったく同じ宣言を処理するためのライブラリーが、他のプログラミング言語用としても実装されることはよくあることです。

上に挙げた例は、すべて、この最初の手法に属するものです。PyLog ライブラリーは、PrologシステムをPythonで実装したものです。このライブラリーは、先ほどの例のようなPrologのデータ・ファイルを読み込み、Prologの宣言をモデルにして Pythonオブジェクトを作成します。EBNFの例では、宣言をmx.TextTools で使用可能な状態表に変換するためのPythonライブラリーであるSimpleParse の特別なバージョンを使用します。mx.TextTools は、それ自体が、Pythonのデータ構造に保存されるけれどもPython自体 とはほとんど関係のないコードを、下部のCエンジンを使って実行するPython用の拡張ライブラリーとなっています。Pythonは、これらの仕事を遂行する上での接着剤 として大きな役割を果たすわけですが、接着される言語は、Pythonとは非常に異なっています。また、Prologのほとんどの実装は、ほとんどのEBNFパーサーと同様、Python以外の言語で記述されています。

DTDも他の例と同様です。xmlproc のような妥当性チェック用のパーサーを使用する場合、DTDを利用してXML文書の方言を検証することができますが、DTDの言語は、Python流のものではなく、xmlproc は、構文解析すべきデータとしてそれ (DTDの言語) を使用しているにすぎません。それに、XMLの妥当性チェック・パーサーは、いろいろなプログラミング言語で記述されてきています。XSLT変換もまた同様で、Python固有のものではありませんし、ft.4xslt のようなモジュールは、Pythonを接着剤として使用しているにすぎません。

上の方法や上で触れたツールが間違っている というわけではありませんが (私もしょっちゅう使用している)、Python自体を宣言型の言語にすることができれば、もっと優美で、いろいろな意味で表現力に富んだことが実現できるのではないでしょうか。他に何もなくても、これを支援するライブラリーがあれば、プログラマーが1つのアプリケーションを開発するときに2つ (またはそれ以上) の言語のことを考える必要はなくなります。Pythonのイントロスペクション機能を利用して「自然な形の」宣言を実装するのが自然で強力な場合もよくあります。


イントロスペクションの魔法

SparkPLY といったパーサーは、ユーザーにPythonの値をPython言語で 宣言させ、ある種の魔法を使って、Pythonの実行時環境に構文解析の構成の役割を果たさせています。たとえば、以前のSimpleParse の文法に相当するPLY のコードで、これを確認してみたいと思います。Spark も、これと同じようなものになります。

リスト5. PLYの例
tokens = ('ALPHANUMS','WORDPUNCT','CONTRACTION','WHITSPACE')
t_ALPHANUMS = r"[a-zA-Z0-0]+"
t_WORDPUNCT = r"[-_]"
t_CONTRACTION = r"'(clock|d|ll|m|re|s|t|ve)"
def t_WHITESPACE(t):
    r"\s+"
    t.value = " "
    return t
import lex
lex.lex()
lex.input(sometext)
while 1:
    t = lex.token()
    if not t: break

私は、近く刊行する予定の本Text Processing in Python で、PLY について執筆していますし、またSpark については、このコラムで取り上げました (リンクについては参考文献参照)。これらのライブラリーについて詳しく立ち入ることはしませんが、ここで注意していただきたいのは、Pythonのバインディングそのものが構文解析の構成 (この例では、実際の字句解析/トークン生成) を行っているという点です。ただ、PLY モジュールの場合は、それが実行されているPython環境について熟知した上で、パターン宣言の処理を行っています。

PLY がその処理内容をどのようにして 知るのかは、非常に高等なPythonプログラミングに関ってくるところです。まず第1段階として、中級プログラマーになると、globals()locals() といった辞書の内容を走査出来ることがわかることと思います。宣言のスタイルが少し異なっていたとしても、問題はありません。たとえば、以下のようなコードになっていたとします。

リスト6. インポートされたモジュールの名前空間を使用
import basic_lex as _
_.tokens = ('ALPHANUMS','WORDPUNCT','CONTRACTION')
_.ALPHANUMS = r"[a-zA-Z0-0]+"
_.WORDPUNCT = r"[-_]"
_.CONTRACTION = r"'(clock|d|ll|m|re|s|t|ve)"
_.lex()

このスタイルは、まったく宣言型でないとは言えないでしょうし、basic_lex モジュールも、仮に、以下のような単純なものにすることができるでしょう。

リスト7. basic_lex.py
def lex():
    for t in tokens:
        print t, '=', globals()[t]

これによって、以下のようなものが生成されることになります。

% python basic_app.py
ALPHANUMS = [a-zA-Z0-0]+
WORDPUNCT = [-_]
CONTRACTION = '(clock|d|ll|m|re|s|t|ve)

PLY は、スタック・フレーム情報を使って、インポート・モジュールの名前空間を調べています。たとえば、以下のようにです。

リスト8. magic_lex.py
import sys
try: raise RuntimeError
except RuntimeError:
    e,b,t = sys.exc_info()
    caller_dict = t.tb_frame.f_back.f_globals
def lex():
    for t in caller_dict['tokens']:
        print t, '=', caller_dict['t_'+t]

これによって、サンプル・コードbasic_app.py と同じ出力が、以前のt_TOKEN スタイルの宣言を用いて生成されます。

実際のPLY モジュールでは、魔法は、これだけにとどまりません。t_TOKEN というパターンで名前の付けられたトークンは、実際には、正規表現からなる文字列か、正規表現のdocstringsとアクション・コードの両方を含んでいる関数のいずれかであることを見ました。型チェックの中には、以下のように、多態的な振る舞いを許すものもあります。

リスト9. polymorphic_lex
# ...determine caller_dict using RuntimeError...
from types import *
def lex():
    for t in caller_dict['tokens']:
        t_obj = caller_dict['t_'+t]
        if type(t_obj) is FunctionType:
            print t, '=', t_obj.__doc__
        else:
            print t, '=', t_obj

もちろん、実際のPLY モジュールは、宣言されたパターンについて、この簡単なサンプル・コードよりももっと面白いことを行うのですが、これらの例にも、いくつかの手法が示されています。


継承の魔法

サポート・ライブラリーがアプリケーションの名前空間を調べ回ったり、操作できるようにすることで、優美な宣言型のスタイルを実現することができます。しかし、イントロスペクションといっしょに継承構造を利用すると、さらに柔軟性の高いことを行えることがよくあります。

モジュールgnosis.xml.validity は、DTDの生成ルールに直接対応させたクラスを作成するためのフレームワークです。gnosis.xml.validity のクラスは、すべて、XMLの方言の妥当性チェックの制約条件に合致する引数を使ってしか インスタンスの作成ができません。しかし実際には、それは、まったく正しくありません。引数を正しい型に「持ち上げる」ための曖昧でない方法が1つしかない場合には、このモジュールは、もっと単純な引数から適当な型を推論することも行います。

gnosis.xml.validity は、私が記述したモジュールですので、その目的自体が面白いのだと、ひいき目に考えているのかもしれませんが、本稿で注目していただきたいのは、妥当性チェック・クラスを作成するときの宣言型のスタイルです。以前のDTDの例に対応するルール/クラスの集合は、以下のようなものになります。

リスト10. gnosis.xml.validityでのルールの宣言
from gnosis.xml.validity import *
class figure(EMPTY):      pass
class _mixedpara(Or):     _disjoins = (PCDATA, figure)
class paragraph(Some):    _type = _mixedpara
class title(PCDATA):      pass
class _paras(Some):       _type = paragraph
class chapter(Seq):       _order = (title, _paras)
class dissertation(Some): _type = chapter

これらの宣言から、以下のようなコードでインスタンスを生成することが考えられます。

ch1 = LiftSeq(chapter, ("1st Title","Validity is important"))
ch2 = LiftSeq(chapter, ("2nd Title","Declaration is fun"))
diss = dissertation([ch1, ch2])
print diss

これらのクラスが以前のDTDに緊密に対応していることがわかります。ネストされているタグの量化 (quantification) や選言 (alternation) のために介在物を使用する必要がある場合を除き、対応付けは、基本的に1対1対応になっています (介在物の名前は、先頭にアンダースコアを付けて区別されます)。

また、これらのクラスが、Pythonの標準的な構文を使って生成されているにもかかわらず、メソッドやインスタンス・データがまったくないという点で特異である (簡潔になっている) 点にも注目してください。クラスは、あるフレームワークから継承を行うためだけに定義され、その際、そのフレームワークは、あるクラス属性によって限定されます。たとえば、<chapter> は、別のタグを連ねたもの、すなわち、<title> に1個以上の<paragraph> タグを続けたものとなります。しかし、ここでは、このように明快な形でchapter クラスを宣言 するだけで、制約条件がインスタンスで守られるようにすることができます。

gnosis.xml.validity.Seq のような親クラスをプログラミングするときに肝心な「技」は、初期化時にインスタンス の.__class__ 属性を調べるということです。クラスchapter 自体には初期化がありませんので、親の__init__() メソッドが呼び出されます。ただし、親の__init__() に渡されるself は、chapter のインスタンスであり、__init__() もそのことをわかっています。これを説明するために、以下にgnosis.xml.validity.Seq のコードの一部を示しておきます。

リスト11. クラスgnosis.xml.validity.Seq
 class Seq(tuple):
    def __init__(self, inittup):
        if not hasattr(self.__class__, '_order'):
            raise NotImplementedError,\
                "Child of Abstract Class Seq must specify order"
        if not isinstance(self._order, tuple):
            raise ValidityError, "Seq must have tuple as order"
        self.validate()
        self._tag = self.__class__.__name__

アプリケーション・プログラマーがchapter のインスタンスを作成しようとすると、インスタンス生成コードは、必要なクラス属性._order を使ってchapter が宣言されているかどうか、さらにこの属性が必要とされるタプル・オブジェクトとなっているかどうかをチェックします。メソッド.validate() は、さらにチェックを行い、インスタンスの初期化に使われたオブジェクトが._order に指定されている対応するクラスに属しているかどうかを確認します。


どんな場合に宣言型プログラミングを行うべきか

宣言型のプログラミング・スタイルは、ほとんどの場合、命令型あるいは手続き型のプログラミング・スタイルよりも直接的に制約条件を記述する方法だと言えます。もちろん、すべてのプログラミングの課題が制約条件に関係するわけではありませんし、少なくとも、これがつねに自然な方法だというわけではありません。しかしながら、文法や推論システムなど、ルールを中心とするシステムの問題は、宣言型の手法で記述できれば、扱いが格段に簡単になります。文法に合致するかどうかを命令型の手法で確認しようとすると、すぐにスパゲッティ・コードになってしまい、デバッグが難しくなります。パターンやルールを記述するほうが、はるかに単純な方法で作業を続けることができます。

もちろん、少なくともPythonの場合、宣言されたルールの確認や適用は、必ず、結局は手続き型のチェックに還元されることになるわけですが、そのような手続き型のチェックは、充分にテストされたライブラリー・コードで行うべきことです。個々のアプリケーションは、SparkPLYgnosis.xml.validity のようなライブラリーで提供される単純な宣言型のインターフェースを利用すればよいわけです。それ以外のxmlprocSimpleParseft.4xslt といったライブラリーも、Python言語による 宣言ではありませんが (これは、それぞれの専門分野からして当たり前のことです)、宣言型のスタイルを可能にしています。

参考文献

コメント

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=Linux
ArticleID=226662
ArticleTitle=魅力的なPython: 宣言型ミニ言語の作成
publish-date=02272003