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

一意型の特徴

前回は、一意型を使って行番号を付加するプログラムを作成しました。これまでに作成した行番号を付加するプログラムを再度振り返ってみます。

モナドの肝は、プログラムはアクションを生成するだけで、アクションを実行するのはプログラムの外部の実行処理系に任されているという点でした。副作用が生じる部分をプログラムの外部の実行処理系がすべて担うために、プログラム自体は関数的に記述することができました。
それに対し、一意型の肝は、副作用が生じる操作を行う場合には、操作対象のリソースへのハンドルを操作のたびに再生成するという点でした。一意型の機構によって、操作の前のハンドルが無効になることで、ハンドルに対する操作が高々1回に制限され、副作用を含むプログラムを関数的なプログラムと見做すことができました。
この「見做す」という部分について、もうすこし説明します。次のプログラムを見てください。

f0, w = open('out.txt', 'w', w)
f1 = fwrites('Good morning, ', f0)    # -- 1
f2 = fwrites('Good afternoon, ', f0)  # -- 2
f3 = fwrites('Good evening, ', f0)    # -- 3
if   t == 0: f4 = f1
elif t == 1: f4 = f2
else       : f4 = f3
f5 = fwrites('World\n',f4)
w = fclose(f5, w)

このプログラムは、tの値に応じて、出力する内容を変更するプログラムを意図しています。そして、関数的なプログラムならば、その意図通りに実行されるはずです。しかし、そのためには、1, 2, 3の操作の間、f0の値(とそれに紐付くファイルの内容)が変化してはいけません。
f0の値(とファイルの内容)が変化しないようにするためには、ファイル操作のたびにファイルのコピーを作成するという方法で実現できます。しかし、ファイルの出力のたびにファイルをコピーするのは非効率です。さらに、fcloseした後に、コピーに対して出力することができることも考えると、正しく制御することは恐ろしく手間のかかることになります。
一意型は、ハンドルに対して高々1回しか操作することができないという制限を加えることで、理論的には毎回ファイルのコピーを作成していると見做しながら、現実には同じファイルを上書き更新するという「最適化」を常に適用することができる仕組みと考えることができます。このような見做しによって、一意型を用いたプログラムは関数的であると言うことができるのです。

話を戻して、モナドメリット, デメリットについて再度整理すると、次のような内容でした。

メリット

  • 言語処理系の内部に実装されたミニ言語の処理系という構成のため、機能の拡張が容易
    • 例)例外・協調的スレッド・ソフトウェアトランザクションメモリなどの拡張が比較的簡単に実装できる
  • モナドという共通のパターンに従っているため、共通的な糖衣構文を適用できる

デメリット

  • アクションを中間に生成する必要があるため、入出力の実行効率が低下する
  • アクションをトップレベルで逐次的にしか処理できないため、命令的なプログラミングスタイルが強制される

それに対し、一意型には次のようなメリット, デメリットが考えられます。

メリット

  • ほとんど実行効率上のペナルティなしに、命令的な入出力関数から、関数的な入出力関数を作成することができる
    • 静的型チェック機構の支援があれば、引数と返値が1つ増えるだけで済む
  • 一意型という共通のパターンに従っているため、共通的な糖衣構文を適用できる
  • 関数的な関数と副作用のある関数を安全に混在させることができるため、プログラムを記述する上での制限が少ない

デメリット

  • 例外のような、処理系に依存した機能拡張を行うには、言語処理系自体を修正する必要がある

また、一意型を採用したシステムの上にモナドを実装することは容易ですが、モナドを採用したシステムの上に一意型を実装する方法は知られていない(例外を使った方法が可能かもしれませんが、効率はよくない)という点は特筆しておいてよいと思います。そのため、今後の連載では、一意型をベースにした議論を進め、必要に応じて、モナドについて言及するという方針としたいと思います。