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

一意型

前回は、モナドのデメリットについて検討しました。その中でも、特に、プログラミングスタイルに関する問題が重要だと考えられます。今回は、この問題を解決するアプローチについて検討します。
まず考えられるアプローチは、runIO()関数をプログラム中で使えるようにすることです。このアプローチは参照透明性を破壊して、プログラムが関数的でなくなる可能性があります。そのため、その利用はライブラリの中に限定して、注意深く利用する必要があります。しかし、どのような利用が安全で、どのような利用が危険であるかを理解するには、プログラムの実行についての深い洞察が必要になります。
モナドに代わって別の仕組みを使うことで、参照透明性を破壊せずに、プログラミングスタイルの問題を解決するアプローチも考えられます。モナドの構造はアクションを逐次的に実行するのに適していますが、その代わりに、アクションを並行的に実行するのに適した仕組みを利用するのです。残念ながら、現在のところ、このアプローチで確立した仕組みはありません。
ここでは、モナドとはまったく別の観点からのアプローチを利用して、プログラミングスタイルの問題を解決することを検討します。そのためには、後でプログラムの意味論を一部修正する必要があります。
基本的な考え方は、シングルスレッド化を利用することです。以前この問題を検討した際には、乱数生成器がシングルスレッド化されることで、プログラムの柔軟性が失われるというデメリットを取り上げましたが、今回は逆にシングルスレッド化を入出力の順序を決定するために利用します。次のプログラムは、この考え方を説明するものです。

def helloworld (w0):
    f0, w1 = fopen ('output.txt', 'w', w0)
    f1 = fwrites ('Hello ', f0)
    f2 = fwrites ('World!', f1)
    w2 = fclose (f2, w1)
    return w2

f0, f1, f2はファイルハンドルです。fwrites()関数は文字列をファイルに出力する関数ですが、この関数は出力した後に次の出力処理のための新しいファイルハンドルを作成して返します。後続の処理では、この新しいファイルハンドルを利用して、出力処理を行います。このようにすることで、出力処理が順番の行われることを保証します。
helloworld()関数自身も、それを呼び出す関数でシングルスレッド化によって順序づけることができるように、スレッド化のためのオブジェクトとして、w0, w1, w2を利用します。このスレッド化オブジェクトを手繰っていくと、トップレベルで定義されたオブジェクトにたどり着きます。これをWorldオブジェクトと呼びます。次のプログラムは、この考えに従って記述したトップレベル関数です。

def main (world0):
    world1 = helloworld(world0)
    return world1

このアプローチは有効そうに見えますが、大きな問題を含んでいます。次のプログラムを見てください。

def helloworld (w0):
    f0, w1 = fopen ('output.txt', 'w', w0)
    f1 = fwrites ('Hello ', f0)
    f2 = fwrites ('World!', f0)
    w2 = fclose (f0, w1)
    return w2

ファイルハンドルオブジェクトのf0が3箇所で使われているため、これらの3つの処理の実行の順序を決定することができず、この関数の参照透明性は成立していません。
この問題を解決するために、プログラムの意味論を修正して、スレッド化オブジェクトを通常のオブジェクトと区別して、ファーストクラスオブジェクトではなく制限つきのオブジェクトとして取り扱うようにします。つまり、スレッド化オブジェクトには、最大1回しかその値にアクセスすることができないという制限を加えます。
この制限を加えることで、上の問題のあるプログラムが正しいプログラムとして受け付けられなくなったことに注意してください。この制限は、上のプログラムのような参照透明性が成立しないプログラムを不正なプログラムとして排除する効果を持ちます。
このアプローチを、一意型と呼びます。一意型は型付けの一種で、この型が付けられたオブジェクトは最大1回しかアクセスできないという制限を受けます。また、一意型が付加されたオブジェクトを、一意オブジェクトと呼びます。