読者です 読者をやめる 読者になる 読者になる

タオルケット体操

サツバツいんたーねっと

Pythonでパターンマッチめいたモノを作ってみる with Rubyのブロック渡しっぽい見た目

python プログラミング DSL

今回説明するものを実装したソースコードは https://github.com/hachibeeDI/masala/blob/master/masala/match.py で読めます。

Rubyで内部DSL的なものを実現する際の常套手段として、ブロックを使うものがあります。
わかりやすい例としては、RakeやSinatraがありますね。
パターンマッチでいうと、egison/egison-ruby · GitHub とかはそんな感じですね。

パターンマッチもどきを実装する前に

さて、PythonにはSinatraに対応するフレームワークとしてFlaskがあります。Flaskではデコレータを使って、Sinatraのような宣言的な見た目を実現しています。ということは、デコレータを使えばブロックと同じことが出来るわけです(?)。
Pythonといえば、switch文がありません。まぁswitch文なんて別にいらないんですが、Rubyのcase式はちょっと羨ましいです。swiftのswitchも悪くないんですが、やっぱが式がいいですよね? まぁ式じゃなくてもいいんですが、とりあず今や時代は猫も杓子もパターンマッチなので、Pythonにも輸入しましょう。

ただ、Pythonは某神の言語みたいに自分で構文を拡張したりは出来ないので、あり合わせの物を組合せなんとかそれっぽいものを作ります。ぶっちゃけ3番煎じくらいのネタなんですけども、今回の実装は既存のものともかなりかけ離れた感じになったので大目に見てください。

さて、私の作ったmasalaライブラリ内にあるMatchクラス式を使えば、以下のような書き方ができるようになります。

使い方の例

とりあえず一例として。リストに対するマッチと、要素への束縛です。
match.endの呼び出しで、渡したブロック(とりあえずそう呼ぶ)のうち評価されたものの値が返ります。

>>> from masala import Match
>>> from masala import Wildcard as _

>>> match = Match([1, 2, 3])
>>> @match.when([2, 2, 2], let_=('one', 'two', 'thr'))
... def case1(one, two, thr):
...     print one, two, thr
...     return one
>>> @match.when([_, 2, 3], let_=('one', '_', 'thr'))
... def case2(one, thr):
...    print 'one: {0} two: {1} thr: {2}'.format(one, '_', thr)
...    return one
one: 1 two: _ thr: 3
>>> match.end
1

他にも、対象へのメソッド呼び出しや演算子評価とかも実装してあります。詳しくはgithubのほうを見てください。もちろんif文にかけることもできます。

実装のキモについて

なかなかキモい感じになってますね?
今回のトリックを実現するための方法ですが、その前にまず

@dec
def hoge(): pass

hoge = dec(hoge)

のシンタックスシュガーだというのは、蛇使いの方であればすでに知っているかと思います。

Pythonの処理系は、関数定義をパースするときに、それがデコレータであれば一度デコレータ部分を解釈します。
普通のデコレータは、

def dec(func):
    def _dec(*args, **kw):
        return func(*args, **kw)
    return _dec

のような感じに、クロージャに包んだ関数を返すように実装します。ですが、

>>> def dec(func):
...    return func()

>>> @dec
... def hello():
...     print('hello')
hello

のような乱暴な実装にすれば、当然デコレートされた関数は、定義された瞬間に中身が実行されることになります。
今回のMatch::whenもそれを応用するような形で実装してあります。

実用性があるかは怪しいところですが、このテクニックを使えば、フレームワークを自分で実装したりする時に色々と面白いことができるかもしれないですね?

なお今回作ったパターンマッチは、まだ思いつき一発で実装しただけなので色々と実用に耐えない感じですが、HaxeやSwiftみたいなVariantっぽいEnumの値への束縛とかそこらへんも実装していきたいなーとかそういう感じです。

本当は実装の詳しいところとかの説明も入れようとしたんですけども、思ったより長くなりそうでめんどかったのでまた今度気が向いたらかきます。

小ネタ

本当はletでの束縛部分は、

@match.when([2, 2, 2]).let_('one', 'two', 'thr')

って書けるようにしたかったんですけど、なんとこれはSyntaxErrorになります。メソッドチェイン出来るようにしておけば、後々whereを実装するときも綺麗に出来たとおもうんだけどなー……

エラーが出たときは一瞬バグを疑ったんですが、どうやら https://docs.python.org/3/reference/grammar.html を読む限り仕様みたいです。

decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE
と宣言されています。マジかよ……
これ僕のような素人考えだと、@の後ろを単純に評価して、返ってきたオブジェクトでデコレートさせた方がよくね? って思っちゃうんですけどなんか深淵的に奥ゆかしい理由があるんですかね。

メタプログラミングRuby 第2版

メタプログラミングRuby 第2版