関数プログラミングのアプローチ (14)

行番号を付加する関数的プログラム (1)

前回、入出力を関数的に取り扱うための仕組みとしてモナドを導入しました。今回はそれを用いて行番号を付加する命令的プログラムを関数的に書き換えます。
プログラムを作成する前に、まず、前回のモナドに修正を加えます。前回のモナドの実装では、fopen()を呼び出すと、ファイルを開いてファイルハンドラを持つIOオブジェクトを作成するため、fopen()を呼び出して返値を捨てるようなプログラムを書くことで、ファイルハンドルを不必要に消費するプログラムを書くことができてしまいます。このことは、2回目以降のfoepn()の呼び出しを失敗させるような副作用を生み出す可能性があるため、十分に関数的な実装になっていませんでした。
修正を行う方法はいろいろ考えられますが、ここでは、Thunkを利用することにします。以前に作成したThunkをベースに、少し修正したものを以下に記述します。

class Thunk:
    def __init__ (self, fun):
        self.fun = fun
    def __get__ (self):
        if self.fun:
            self.value = get(self.fun())
            self.fun = False
        return self.value

def get (x):
    if isinstance(x, Thunk):
        x = x.__get__()
    return x

このThunkを用いて、IOモナドやfopen()などの入出力関数を書き直します。さらに、出力関数も追加します。

class IO:
    def __init__ (self, v):
        self.args = v
    def bind (self, act):
        return Bind(self, act)

class Bind:
    def __init__ (self, io, act):
        self.io = io
        self.act = act
    def bind (self, act):
        return Bind(self, act)

def io_return (*v):
    return IO(v)

def fopen (fname, mode):
    return IO(Thunk(lambda: [open(fname, mode)]))

def freadline (ifile):
    return IO(Thunk(lambda: [ifile.readline()]))

def fwrite (ofile, text):
    def f ():
        ofile.write(text)
        return []
    return IO(Thunk(f))

def fclose (file):
    def f ():
        file.close()
        return []
    return IO(Thunk(f))

さらにトップレベルを次のようにThunkを評価するように書き直します。

def runIO (b):
    while isinstance(b, Bind):
        args = runIO(b.io)
        b = apply(b.act, args)
    return get(b.args)

import sys
print runIO (IO(sys.argv).bind(actions))

これらを用いることで、行番号を付加する命令的プログラムを次のように書き換えることができます。

def actions (prog, ifname, ofname):
    return \
    fopen(ifname, 'r')    .bind(lambda (ifile): \
    fopen(ofname, 'w')    .bind(lambda (ofile): \
    lineNum(ifile, ofile) .bind(lambda: \
    fclose(ofile)         .bind(lambda: \
    fclose(ifile)         .bind(lambda: \
    io_return())))))

def lineNum (ifile, ofile):
    def loop (n):
        def cond (line):
            if line:
                return \
                fwrite(ofile, str(n))  .bind(lambda: \
                fwrite(ofile, " ")     .bind(lambda: \
                fwrite(ofile, line)    .bind(lambda: \
                loop(n+1))))
            else:
                return io_return()
        return freadline(ifile).bind(cond)
    return loop(1)