このBlogは移転しました。今後は aish.dev を御覧ください。

PythonはDSLが苦手?

PythonDSLが苦手でしょうか、って話をチラッと目にした。あんまりDSLって知らないけど、そんなに得意ってことはないんじゃないかと思う。Pythonってのは構文的に遊びが少ないように考慮して作られてるし、そもそもPythonは「実行可能な擬似コード」って言われるぐらいにシンプルなんで、わざわざ新しくDSLを仕立てなくても、って気もする。

ただまあ、工夫の余地がないかというとそうでもなくて、例えば

if not INPUT:
    EXIT
SORT
REVERSE
PRINT

のような、独自言語っぽい見栄えのコードを書けないってこともない。上のコードは、コンソールから一行読み込んで、文字をソートしてひっくり返し、コンソールに出力するPythonスクリプトだ。

えーと、上のコード、Pythonスクリプトと言ったのは嘘ではないけど、実行時にちょっと細工がいる。こんな感じで起動しなければならない。

class DSLExit(Exception):
    pass

class DSLHelper(dict):
    notfound = object()
    def __getitem__(self, name):
        ret = self.getValue(name)
        if ret is self.notfound:
            ret = super(DSLHelper, self).__getitem__(name)
        return ret

    def __setitem__(self, name, value):
        if not self.getValue(name, value):
            return super(DSLHelper, self).__setitem__(name, value)

    def getValue(self, name):
        return getattr(self, name, self.notfound)
    
    def setValue(self, name, value):
        setattr(self, name, value)
        return True

class Simple(DSLHelper):
    VALUE = ''

    @property
    def INPUT(self):
        self.VALUE = raw_input(">")
        return self.VALUE

    @property
    def REVERSE(self):
        self.VALUE = self.VALUE[::-1]

    @property
    def SORT(self):
        self.VALUE = "".join(sorted(self.VALUE))

    @property
    def PRINT(self):
        print self.VALUE

    @property
    def EXIT(self):
        raise DSLExit()

    def getValue(self, name):
        return getattr(self, name, self.notfound)

s = """
if not INPUT:
    EXIT
SORT
REVERSE
PRINT
"""

try:
    exec s in Simple()
except DSLExit:
    pass

つまり、実行時のネームスペースとして専用オブジェクトを指定し、値が参照されたら特定のアクションを実行するように仕込んで置く訳だ。

ただ、この方式だと引数付きのコマンド実行はできない。普通に

COMMAND(ARG1, ARG2)

で良いじゃんと思うのだけど、

COMMAND ARG1 ARG2

じゃないとDSLっぽくないからダメなんだそうだ。まあ、頑張ればそれっぽくならないこともない。例えば、

mkdir, "abc"
touch, "abc/def"

のように、項を "," で区切る方式はどうだろう。これだったら、みんなお馴染みのastモジュールを使えば何とかなりそうだ。

import os, ast
class ShellCmd(DSLHelper):
    def getValue(self, name):
        ret = super(ShellCmd, self).getValue(name)
        if ret is self.notfound:
            def ret(*args):  # ret の値を関数に置き換える暗黒Hack
                return os.system(" ".join((name,)+args))
        return ret

class _Transform(ast.NodeTransformer):
    def visit_Tuple(self, node):
        ret = ast.copy_location(
                ast.Call(node.elts[0], node.elts[1:], [], None, None),
                node)

        return ret

m = compile(
"""
mkdir, "abc"
touch, "abc/def"
""", "filename", "exec", ast.PyCF_ONLY_AST)

_Transform().visit(m)

try:
    exec compile(m, "filename", "exec") in ShellCmd()
except DSLExit:
    pass

何をしているかというと、タプルが生成されるときにタプルを作らず、先頭の要素を関数と見なし、二番め以降の要素を引数として呼び出している。だから、このDSLにカッコをつけてもちゃんと実行される。

(mkdir, "abc")
(touch, "abc/def")

まあ、こう書いてしまうとLisp使えって話になってしまうがw