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

モナド (Monad)

前回は、関数的に入出力を表現しながら、制御構造を表現する方法を検討しました。今回は、このアプローチを進めて、組み立て可能なソフトウェア部品として利用可能な構成について検討します。
前回の処理がソースコードにハードコードされていました。しかし、各アクションはそれぞれ別々の関数として独立しているため、これらのアクションを別々に定義して必要に応じて組み合わせて利用できるよう部品化することができます。また、部品化のついでに、トップレベル以外でアクションを実行することができないように、アクションをカプセル化することを同時に行います。
ここでは、部品化の仕掛けとして、モナドを利用します。まず、次のようなコンテナを用意します。カプセル化によって、プログラム中でコンテナから値を取り出すことは、できないと仮定します。

class IO:
    def __init__ (self, *v):
        self.args = v
        self.actions = None

そして、各アクションについて、引数 i, 返値 oを持つアクションについて、iを受け取って、IO(o)を返す関数として定義します。

action(i) -> IO(o)

例えば、ファイルを開くアクションは、引数としてファイル名とモードを受け取り、ファイルハンドルを返すので、次のような関数として定義することができます。

def fopen (fname, mode):
    return IO(open(fname, mode))

さらに、アクション同士を連結するために、IOクラスのbind()メソッドとBindクラスを作成します。また、プログラムの読みやすさのために、io_return()関数を補助的に作成します。

class IO:
    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 apply(IO, v)

このようにすることで、前回、前々回のファイルを1行読むプログラムを、次のように記述することができます。

def actions (prog, fname):
    return \
    fopen(fname, 'r')    .bind(lambda (ifile): \
    freadline(ifile)     .bind(lambda (line): \
    fclose(ifile)        .bind(lambda: \
    io_return(line))))

def freadline (ifile):
    return IO(ifile.readline())

def fclose (file):
    file.close()
    return IO()

このプログラムを読むにはbind()メソッドの前後で、

action    .bind(lambda (result): \

というところを、actionを実行して、その結果をresultで受け取るというように考えます。また、io_return(value)は、valueを返す(return)だけのアクションと考えます。
bind()を用いることで、アクションをつなぎ合わせることができます。例えば、次のプログラムは、メインルーチンでファイルを開いて、サブルーチンの処理を呼び出し、サブルーチンの結果を受け取って、ファイルを閉じるというプログラムの骨格を記述したものです。

def actions (prog, fname):
    return \
    fopen(fname, 'r')    .bind(lambda (ifile): \
    subroutine(ifile)    .bind(lambda (ret): \
    fclose(ifile)        .bind(lambda: \
    io_return(ret))))

例えば、次のようなsubroutine()を作成することで、ファイルから2行のデータを読み込むことができます。

def subroutine (ifile):
    return \
    freadline(ifile)     .bind(lambda (l1): \
    freadline(ifile)     .bind(lambda (l2): \
    io_return([l1,l2])))

ファイルの全ての行を読み取るには、次のように書くことができます。

def subroutine (ifile):
    def loop (ret):
        def cond (line):
            if line:
                return loop(ret + [line])
            else:
                return io_return(ret)
        return freadline(ifile).bind(cond)
    return loop([])

loop()内で、freadline()を一回呼んで、その結果をcond()で受け取って次のloop()を呼び出すかどうかを判断するアクションを生成しています。このようにすることで、ファイルの終端に達するまで連続的に次の行を読み込むアクションを生成して、ファイルの内容をすべて読み取ることができます。
このように作成したアクションを実行するためのトップレベルは次のように書きます。

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

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

モナドを利用することで、入出力の表現とその実行を分離して、入出力をアクションの組合せとして表現し、アクションの実行をトップレベルのみに限定するという方法で、柔軟性のある入出力の表現を関数的に記述することができました。このようなアプローチとしてモナドはいくつかあるアプローチの1つです。モナド以外のアプローチとしては他にアロー(Arrow)などが存在します。