勝手にチェインとか名付けてみる

前回のエントリで、関数をreduceすると逐次的な処理を関数的に記述できると書きました。その話題を広げようと。

関数を合成するためのユーティリティ関数群を書きました。主な関数は

  • chain リスト中の関数を合成していく関数。高階関数をreduceに渡して関数を返す高階関数
  • debugp 関数の間で受け渡しされるデータをダンプする関数。デバッグのために使う。表示されるオブジェクトは__repr__なんかのメソッドで副作用が無いと仮定している
  • debugpm メッセージを引数にとり、データをダンプする関数を返す関数。メッセージ付きのデバッグに使う。
  • insert_debug リストのindex番目とindex+1番目の関数の間にデバッグ関数を挿入する。
  • stripe_debug リストのすべての関数の間にデバッグ関数を挿入する

chainで合成された関数のリストをチェインと呼ぶことにします。かっこいいから。

チェインの要素はすべて1引数関数です。引数が必要ない関数をチェインに加えるときは、

lambda x: func()

としてください。逆に、引数の数が複数あるときは

lambda x: func(x[0], x[1])

などとしてください。

# -*- coding: sjis -*-
# composit.py
"""module for proccessing sequencial proccess as
chain of function.
"""

id = lambda x: x
def p(x): print x

# compositioning code
def chain(flst):
    def composit(f, g):
        def fn(x):
            return g(f(x))
        return fn
    return reduce(composit, flst)

# debugging code
def debugp(x):
    print x
    return x

def debugpm(message):
    def debugging(x):
        print message, x
        return x
    return debugging

def insert_debug(index, flst):
    lst = flst[:]
    ins = debugpm(lst[index].func_name+":\t")
    lst.insert(index+1, ins)
    return lst

def stripe_debug(flst):
    lst = []
    for i in range(len(flst)-1):
        ins = debugpm(flst[i].func_name+":\t")
        lst.append(flst[i])
        lst.append(ins)
    lst.append(flst[-1])
    return lst


if __name__ == '__main__':
    def f(x): return x ** 2
    def g(x): return 2*x, 3*x
    def h(x, y): return (x + y) / 2.0
    def ff(x): return h(x[0], x[1])
    lst = [
        f,
        g,
        ff,
        f,
        g,
        ff
        ]

    # insert debug functions
    lst = stripe_debug(lst)
    composit = chain(lst)
    print [composit(x) for x in range(1,6)]

実行結果

$ py composit.py
f:      1
g:      (2, 3)
ff:     2.5
f:      6.25
g:      (12.5, 18.75)
f:      4
g:      (8, 12)
ff:     10.0
f:      100.0
g:      (200.0, 300.0)
f:      9
g:      (18, 27)
ff:     22.5
f:      506.25
g:      (1012.5, 1518.75)
f:      16
g:      (32, 48)
ff:     40.0
f:      1600.0
g:      (3200.0, 4800.0)
f:      25
g:      (50, 75)
ff:     62.5
f:      3906.25
g:      (7812.5, 11718.75)
[15.625, 250.0, 1265.625, 4000.0, 9765.625]

チェインは、初めに入力されたデータを次々と加工して結果を返すパイプラインです。
つまり、ある関数の返り値は次の関数の引数になります。チェインに引数を無視する関数がある場合、それまでのデータの流れは止まり、新たなデータの流れが始まります。関数の間でやりとりされるデータはdebugp, debugpm関数でモニタできます。

ここまで書いてふと思ったのですが、やりたい処理をリストして順次実行するパイプライン。このやり方はHaskellモナドに近いやり方ではないでしょうか。引数を無視する要素はHaskellの>>、無視しないのは>>=。

このモジュールを使って、webプログラミングでありがちな金融情報を取得して携帯にメールを送信するプログラムを書いてみました。プログラム中のgmailモジュールはgmailからメールを送信するモジュールで、
http://www.unoh.net/mt32/unoh-mt32-ja-tb.cgi/877
を参考に作成したものです。

#! /usr/bin/python
# -*- coding: utf-8 -*-

import urllib, re
import composit

YAHOO_FINANCE = "http://quote.yahoo.co.jp"

def getpage():
    content = None
    try:
        content = urllib.urlopen(YAHOO_FINANCE).read()
    except:
        raise "?"
    return content
    
def filterinfo(content):
    expr = re.compile(r"""<small>(?:<b>)?(?:<font color=ff0020>)?(.*?)(?:</b>)?(?:</font>)?</small>""")
    lst = expr.findall(content)[1:]
    info = []
    for i in range(5):
        info.append( [lst[i*3], lst[i*3+1], lst[i*3+2]] )
    info.append( [lst[-2], lst[-1]] )
    return info

def pp(info):
    # construct string to be pretty printed
    lst = ["\t".join(n) for n in info]
    return "\n".join(lst)

def mailtome(subject, body):
    import gmail
    f = gmail.MY_ADDRESS
    t = gmail.PHONE_ADDRESS
    msg = gmail.create_message(f, t, subject, body)
    gmail.send_via_gmail(f, t, msg)


lst = [lambda x:getpage(),
       filterinfo,
       composit.debugpm("****** filtered info ****** \n"),
       pp,
       composit.debugpm("****** result ******\n"),
       lambda x: unicode(x, "euc_jp"),
       lambda x: mailtome(u"マーケットサマリー", x),
       ]
main = lambda: composit.chain(lst)(None)

if __name__ == '__main__':
    main()

チェインを弄った感想。まず、デバッグが楽。それと、心なしか関数が適切な粒度になるように強制される感があります。
リストに実行すべき関数を順番に記述するプログラミングのやり方。面白く無いですか?