2009年04月19日

Haskellの'$'みたいなのを作ってみる

 関数型プログラミング言語Haskellは色々と面白い特徴があるのだが、その中でも関数適用で
	f1(f2(f3(arg)))
	f1 $ f2 $ f3 $ arg
と書けてしまう'$'演算子が結構良い感じだ。
 Pythonではあいにく'$'は演算子として認められていないので、他の演算子を選ぼうとしたのだが、実は候補は一つしかなかった。
 それは'**'だ。
 Pythonの微妙な仕様の一つに、演算子のオーバーロードは出来ても、演算子の演算順序を変えることは出来ないという法則がある。
 そして調べたところ、同一の演算子で$演算子のように「右結合」の演算子は、Pythonでは'**'しかなかった。
 これを調べる方法は簡単だ。
 まず、前に書いたシンボルを応用して、演算子結合調査用のクラスを書く。
#sss.py

class S:
  def __init__(self, s):
    self.s = s
  def __repr__(self):
    return self.s
  def __add__(self, other): return S("("+self.s+other.s+")")
  def __mul__(self, other): return S("("+self.s+other.s+")")
  def __truediv__(self, other): return S("("+self.s+other.s+")")
  def __sub__(self, other): return S("("+self.s+other.s+")")
  def __floordiv__(self, other): return S("("+self.s+other.s+")")
  def __and__(self, other): return S("("+self.s+other.s+")")
  def __or__(self, other): return S("("+self.s+other.s+")")
  def __xor__(self, other): return S("("+self.s+other.s+")")
  def __lshift__(self, other): return S("("+self.s+other.s+")")
  def __rshift__(self, other): return S("("+self.s+other.s+")")
  def __mod__(self, other): return S("("+self.s+other.s+")")
  def __pow__(self, other): return S("("+self.s+other.s+")")
  def __eq__(self, other): return S("("+self.s+other.s+")")
  def __ne__(self, other): return S("("+self.s+other.s+")")
  def __gt__(self, other): return S("("+self.s+other.s+")")
  def __lt__(self, other): return S("("+self.s+other.s+")")
  def __le__(self, other): return S("("+self.s+other.s+")")
  def __ge__(self, other): return S("("+self.s+other.s+")")

a = S('a')
 これは、演算子を適用した結果、どちらに結合するかを記したものである。
 通常の左結合なら、以下のようになる。
>>> from sss import a
>>> a+a+a
((aa)a)
>>>
 やってみればわかるが、'-'を使おうが'*'を使おうが、全て同じ結果となる。
 それに対し、右結合なら、このようになる。
>>> a**a**a
(a(aa))
>>>
 この結果が出た演算子が'**'だけだった、というわけだ。
 なお、比較演算子も調査したが、比較演算子は全て一つとして連結してしまうので、全く使い物にならない。
 というわけで、選択の余地なく'**'に決定した。
 さて、実装の方法としては、ラッパーのクラスを書き、デコレータで関数に実装するのが簡単だろう。
# dollar.py
class DL:
  def __init__(self, f):
    self.f = f
  def __pow__(self, other):
    return self.f(other)
  def __call__(self, arg):
    return self.f(arg)
 今回は目的の内容により、1引数の関数のみをラッピングすることにする。
>>> @DL
... def f(v): return v + 'f'
...
>>> f('X')
'Xf'
>>> f ** 'X'
'Xf'
>>> @DL
... def g(v): return v + 'g'
...
>>> @DL
... def h(v): return v + 'h'
...
>>> f ** g ** h ** 'X'
'Xhgf'
>>> h ** g ** f ** 'A'
'Afgh'
>>>
 組み込みのものは、デコレータと同じことをすれば良い。
>>> lenD = DL(len)
>>> lenD ** f ** g ** h ** 'A'
4
>>>
 はっきり言えば、この方法はreduceの代替にすらならないし、以下のような関数を書けば、それで事足りてしまう。
>>> def DDL(arg, *f):
...   for x in f:
...     arg = x(arg)
...   return arg
...
>>> DDL('X',f,g,h)
'Xfgh'
>>>
 しかし、関数プログラミングによる多数の関数の連続適用を少しでもシンプルに書くために、こういった方法を模索するのも悪くないのではないかと思う。
 また、Haskell式を無理にしなくとも、上の関数のように前から順に適用していくのでれば、別の演算子を実装する方法はある。
class DL2:
  def __init__(self, f):
    self.f = f
  def __rmul__(self, other):
    return self.f(other)
  def __call__(self, arg):
    return self.f(arg)
 としておいて…。
>>> @DL2
... def f(v):return 'f' + v
...
>>> @DL2
... def g(v):return 'g' + v
...
>>> @DL2
... def h(v):return 'h' + v
...
>>> 'X' * f * g * h
'hgfX'
>>>
 ただ、関数が左結合なので、気持ち悪さは隠せない。
 順序を明示するために、演算子を'>>'あたりにするのが妥当かもしれない。
class DL3:
  def __init__(self, f):
    self.f = f
  def __rrshift__(self, other):
    return self.f(other)
  def __call__(self, arg):
    return self.f(arg)
 として…。
>>> 'X' >> f >> g >> h
'hgfX'
>>>
 なんとなくC++を思い出して妙な気がするのは、私だけだろうか。

この記事へのトラックバックURL

http://trackback.blogsys.jp/livedoor/kikwai/51588251
この記事へのコメント
>なんとなくC++を思い出して妙な気がするのは、私だけだろうか。
HaskellのArrowを思いだしました。
Posted by   at 2009年05月01日 18:19
 Arrowというのを知らなかったので、調べて……

……

 わかんないす、コレ(泣)

※Haskellは触ったばかりの、まだうろ覚えです、はい
Posted by 機械伯爵 at 2009年05月06日 13:27