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

入出力をプログラムから分離する

前回、入出力を含むプログラムがそのままでは関数的ではないため、関数プログラミングで入出力を扱うには特別な方法が必要になるということを説明しました。しかし、どのようにすれば、評価のタイミングによって結果が変わる処理を、関数プログラミングの枠内で取り扱うことができるのでしょうか?
1つのアプローチとして、プログラムの中では「何を実行すべきかを指定するだけで、実際には実行しない」という方法があります。この説明は抽象的で、一見するとまるで禅問答ですが、アプローチとしてはシンプルなものです。次の例を見てください。

def second (a,b): return b

actions = [
  lambda (fname)        : open(fname, 'r'),
  lambda (ifile)        : (ifile, ifile.readline()),
  lambda ((ifile, line)): second(ifile.close(), line)
  ]

これは、与えられたファイル名のファイルを開いて、1行読み込んで、ファイルを閉じて、読み込んだ値を返す、というプログラムを上のアプローチで記述したものです。actionsがプログラムの本体で、処理を1ステップ毎に分解し、それぞれをlambda式で記述して、処理する順番にリストの要素に並べたものです。注目すべきことは、このプログラムはどのような順番で評価しても、同じlambda式のリストが得られるという意味で関数的であるということです。
このプログラムは、確かにプログラムが何を行うべきかは完全に表現できていますが、このままでは実際の入出力が行われることはありません。このプログラムを実際に動作させるには、別途、次のような手続きを用意する必要があります。

def runIO (actions, args):
    for a in actions:
        args = a(args)
    return args

runIO()は、与えられたactionsのリストを、先頭から順番に実行して、最後に結果を返すという手続きです。これを利用するには、次のように呼び出します。

runIO(actions, 'temp.txt')

すると、実際に入出力が行われて、temp.txtファイルの1行目の値を取得することができます。
注意すべきことは、runIO()自体は関数的ではないということです。これは、次のようなプログラムを考えることで分かります。

writeActions = [
  lambda ((fname, text)): (open(fname,'w'), text),
  lambda ((ofile, text)): second(ofile.write(text), ofile),
  lambda (ofile)        : ofile.close()
  ]

runIO(actions, 'temp.txt') # -- (1)
runIO(writeActions, ('temp.txt', 'over-write'))
runIO(actions, 'temp.txt') # -- (2)

このプログラムの(1)の式と(2)の式は、全く同じ式であるにもかかわらず、違う結果になる可能性があります。actionsやwriteActionsは全く関数的であるにもかかわらず、runIO()が関数的でないために、結果として関数的でないプログラムとなってしまっています。
このようなことから、このアプローチで関数プログラミングを行う場合は、runIO()をプログラムの内部で利用することはできないということが分かります。逆に言えば、runIO()をプログラムの外部に追い出してしまうことによって、入出力を含むプログラムを関数的に記述することができるということになります。
つまり、プログラムの目的を、actionsリストを作成することと定義しなおして、main()関数の返値としてactionsリストを返せば、言語の処理系がactionsリストを評価して、実際にプログラムを実行するという構成とするのです。この場合、runIO()は言語処理系に組み込まれた仕組であって、プログラム内部で利用可能な手続きとしては提供されていないと考えることができます。