関数プログラミングのアプローチ (15)
モナドを使うメリット
前回はモナドを用いて、行番号を付加する関数的なプログラムを作成しました。この関数的なプログラムは、以前作成した命令的なプログラムと比較して、複雑な仕組みになってしまいました。このような複雑な仕組みを導入することに何かメリットがあるのでしょうか?
モナドの処理系は、プログラミング言語の実行環境の内部に実装されたミニ言語の実行系のように機能します。つまり、モナドというパターンを用いることで、比較的手軽に言語内言語を作成することができるのです。前回使ったIOモナドは、入出力を行うための言語内言語と考えることができます。
プログラムのコードがそのまま直接実行されるのではなく、一度アクションとして表現されることで、処理系を拡張することが容易になります。たとえば、次のような実装を追加することで例外を送出することができます。プログラム中でエラーが発生した場合に、例外を送出することでトップレベルまで一度に脱出することができます。try...catch...のような仕組みも、少しの修正で実装することができます。
class Error: def __init__ (self, msg): self.msg = msg def throw (msg): return Error(msg) def runIO (b): while isinstance(b, Bind): args = runIO(b.io) if isinstance(args, Error): return args b = apply(b.act, args) if isinstance(b, Error): return b return get(b.args) import sys ret = runIO (IO(sys.argv).bind(actions)) if isinstance(ret, Error): print "Error: " + ret.msg else: print ret
他にも、協調的スレッドやソフトウェアトランザクションメモリなどの機構も、モナドをベースにすると比較的容易に実装することができるようになります。たとえば、簡単な協調的スレッドは、複数のIOモナドからアクションをラウンドロビン方式で取得して、順番にアクションを実行することで実装することができます。
ところで、前回のプログラムは、あまり読みやすく、書きやすいプログラムであるとは言えません。しかし、文法を工夫することで読みやすさ・書きやすさは改善することができます。たとえば、次のようなPythonのプログラムは、
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)
Haskellで表現すると次のように書くことができます。
lineNum ifile ofile = loop 1 where loop n = freadline ifile >>= \line -> case line of Nothing -> return () (Just str) -> fwrite ofile (show n) >> fwrite ofile " " >> fwrite ofile str >> loop (n + 1)
さらに、do記法という糖衣構文を用いることで、次のように書くことができます。
lineNum ifile ofile = loop 1 where loop n = do line <- freadline ifile case line of Nothing -> return () (Just str) -> do fwrite ofile (show n) fwrite ofile " " fwrite ofile str loop (n + 1)
モナドという共通のパターンに従うことで、異なる言語内言語処理系の実装に対して、同じ糖衣構文を共通的に利用することができることも、モナドのメリットの1つです。