アクションと関数、式の値の話
a -> b
というのは、引数にa
型の値をとり、b
型の値を返す関数の型だ。Monad m
が存在するような任意の型コンストラクタm
について、m a
というのは、a
型の値を生成する(Monad m
の)アクションの型だ。
という型の意味を理解していれば、関数(>>) :: Monad m => m a -> m b -> m b
は、その型から、
Monad m
が存在するような任意の型コンストラクタm
について、m a
型の値(アクション)(これをx
とする)を引数にとり、m b -> m b
型の値(関数)(これは(>>) x
と表せる)を返す関数で、つまりその返される関数は、m b
型の値(アクション)(これをy
とする)を引数にとり、m b
型の値(アクション)(これは(>>) x y
と表せる)を返す関数で、つまりその返されるアクションは、b
型の値を生成するアクション
だということが読み取れる。
一方で、「m a
型の値(アクション)x
を破棄する」といったことは、型から読み取ることはできないし、事実でもない。実際、(>>) x y = (>>=) x (\z -> y)
と定義でき、アクションx
は破棄されない。破棄されるのはアクションx
が生成する値で、それはx
ではなくz
だ。
関数が値を返すということと、アクションが値を生成するということには大差がないように見える。実際に、多くのプログラミング言語は関数に副作用を認めていて、関数とアクションを区別しない。しかし、関数が、引数が定まれば返り値も一意に定まるという性質を持っているのに対し、アクションは、生成する値が定まるという性質を持っていないから、同列に扱おうとすると、式の値が一意に決まらなくなる、という困ったことが起きる。
そもそも、式の値はなんだろうか。(ここで書いている「手続き型プログラミング」「関数型プログラミング」という分け方は一面的なものに過ぎないから、あんまり深刻に考えないで欲しいのだけれども、)手続き型プログラミングにおいて、値というのは手続きから手続きへ渡されたり、変数に代入されたり読み出されたりするものであって、「式が値を持つ」という考えは、算術の式に対しては考えていても、プログラム一般に敷衍されるものではないように思われる。プログラムは文の集合だ。文は意味を持つが値を持たない。それが、手続き的なプログラミングにおいて一般的な考え方だ。
一方、関数型プログラミングでは、プログラムは式だ。関数型プログラミング言語の式は部分式と関数適用から構成されていて、どの部分式も値を持つ。関数型プログラミングでは、プログラムの構造を内包した値を扱うことでプログラミングするようになる。直和型の値は分岐構造を内包しているし、リスト型の値は反復構造を内包している。モナドは、手続き型言語における各種副作用を持つ、逐次構造を内包した型の集まり(型クラス)ということになる。このように複雑な値を使うことで、関数型プログラミングではプログラムの意味と式の値を同一視できるようになる。
プログラムの意味と式の値を同一視できると何が嬉しいかというと、意味を持つプログラムの断片がすべて値を持つので、プログラムを処理するプログラムが書きやすくなる。プログラムのどの断片も、関数として抽象化することができる。他の関数に渡すことが出来る。変数にプログラムを代入できる。
話をアクションに戻す。Haskellにおいては、アクションが生成する値と、アクション自体は別だ。たとえば、getChar :: IO Char
は標準入力から文字を1文字読み込み、その文字に対応するChar
型の値を生成するアクションなのだけれども、Haskellでは、式getChar
の値は「標準入力から文字を1文字読み込み、その文字に対応するChar
型の値を生成するアクション」自体であり、生成されたChar
型の値ではない。アクションgetChar
の生成する値を取り出して言及するには、do {z <- getChar; k z}
のz
、getChar >>= \z -> k z
のz
とでも言うしかない。
色々書いたけど、結局、値とは何かという話に合意が得られていない状況ではどうにも話が通じないという点が問題で、以前からココらへんの考え方を整理して参照透過性についてなんとか説明しようと試みたりしているのだけれども、やっぱり難しいですね。