アクションと関数、式の値の話

bugrammer.hateblo.jp

  • 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}zgetChar >>= \z -> k zzとでも言うしかない。

色々書いたけど、結局、値とは何かという話に合意が得られていない状況ではどうにも話が通じないという点が問題で、以前からココらへんの考え方を整理して参照透過性についてなんとか説明しようと試みたりしているのだけれども、やっぱり難しいですね。

追記のメモ書き

ツッコミどころ

  • Haskellにも値を持たない、式より上位の宣言の構造があるじゃん
  • プログラムの意味と値が一致しない純粋でない関数型言語なんてザラでしょ
  • 値を返す手続きを第一級の値として持てれば、純粋でなくても高度なプログラミングが簡単にできる上に、手続きを取り扱うだけのためにモナドを取り入れる必要がない。プログラムの意味と値を一致させる必要ってないんじゃないのか。
  • モナド以外に、一意型を使って値の複製を制限し、制御(ハンドラ)を表す値を導入することで、プログラムの意味と値の一致を図る方法がある。