PythonはDSLが苦手?
PythonはDSLが苦手でしょうか、って話をチラッと目にした。あんまり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