副作用

ここでは、haskellの動作ではなくて、プログラムの一般的なことを説明するよ。
リンクを貼っておいて、こう書くのも難だけど、ここを読むと理解が混乱するかも知れない。
一応、言語の根幹に関わることだから説明するけど、深く考えない方が良い気がするよ。


副作用(side effect)というのは、メッセージの表示や、変数への代入といった、状態が変化する作用のことを言うんだ。
そうでない作用をする関数を純関数というけど、用語の対応は良く分からない。

ともあれ、副作用は関数型言語を使っていると良く問題にされるんだ。
関数型言語ではない言語は、大抵副作用を持っている命令で構成されている。
変数に計算結果を代入して、それを表示する……のようにね。

でも、関数型言語はそうじゃない。
プログラムの大半は値の宣言で出来ている。
とりあえず、説明のためにCの関数(と呼ばれているもの)とhaskellの関数(と呼ばれているもの)を示そう。

//Cの関数
int fc(int x){return (x*x);}

--haskellの関数
fh x = x*x

このfcとfhは、それぞれの言語でほぼ同じように扱われる。けど、その意味合いは少し違う。
fcはxを与えたらx*xを返せ、と言う命令だ。この命令自体はintと同様に扱えることを明示している。

一方、fh xは値である。この宣言は、実はfh xに対するものなのだ。そう読むと、本当にそのままを表していることが分かる。
ちなみに、fhという関数そのものを宣言したいのであれば、まだ出てきてない表現だけど、

fh = \x -> x

と書くことになる。実質的には上の定義と同じになる。
要は、上の定義だと、fh xがx*xという値として宣言され、そこから推論された結果、fhはxを引数にx*xを返す関数だとされるのである。
で、型推論の結果、xに入る値は*が使える値、つまり数値に限定される。

見た目は似ているけど、意味は大分違う。もちろん、使われ方も違う。
Cの命令は、それ自体が副作用である。returnは、命令を呼び出した先に値を返すという動作を示す。
こういう書き方をしている都合上、他に副作用があるか、命令を使う側からすると判断が付かない。

一方、haskellの定義は単に、値に名前を付けているだけである。値だから、型さえ合っていれば好きに使って良いことが分かる。
値なんだから、副作用なんかあるわけがない。

……そう、haskellは値の定義しかできないのだから、副作用なんか持たないのだ。
この重大な特長のために、純関数型言語と呼ばれている。
それは理解できたけど、なんか怪しい。
プログラミング言語なのだから、変数を使ったり動作をさせたり出来なくちゃおかしい。
実際に触って確かめたように、画面の表示も出来るわけだし(変数はまだ見てないけど)。

では、謎を解明するために、明らかに副作用を持つ、画面を表示するプログラムで考えてみよう。

//C:簡単のため、宣言は省略
int main(void){printf("Hello,world!");return 0;}

--haskell
main = putStrLn "Hello,world!"

これをコンパイルすると、動作が同じプログラムが出来ると思う。haskellの方が容量は大きいけど(ぁ
main関数を読んでない人は、それを参考にするか、フィーリングで理解してね。

Cは明らかに動作を記述している。mainが呼び出されると、printf命令が実行される。
これは明らかに値ではないよね。値がむき出しに書かれているというのはおかしい。
で、どこかに向かって0を返している。
私はCには詳しくないので良く分からないけど、関数の形をしているから必要なんだろう。

そして、問題のhaskellである。
実行結果はCと同じなんで、それだけでは何だかわからないから、GHCiで調べてみよう。
まあ、printで調べたのと同じ気がするけど。

*Main> :t main
main :: IO ()

そう、mainはIO ()という値なんだ。IOは型の名前だ。型の名前がIOでも、値は値だから、これが何かをしているわけじゃない。
Cのreturn 0と同様、どこかにIO ()を投げているだけだ。
説明書を読むと、これは無視するものなんだろう。

どうも、このソースコードを見ていると、IO ()が評価されると、そのタイミングで評価器がアクションを返すようだ。
haskellは遅延評価だから、呼び出されたときに評価されるわけだし。
そのアクションに対して、haskellのコードは無関係でいられる。だから、副作用がないかのようにソースを書けるんだ。

Cとhaskellのプログラムの動きをまとめてみよう。

C:
  1. システムがCのプログラムをコールする。
  2. mainの命令を実行する。中身は次の通りである。
    1. printfを実行する。
    2. 値 0 を返す。

haskell:
  1. システムがhaskellのプログラムをコールする。
  2. mainの値を評価する。このとき、評価系がmainで評価された値に対して何らかのアクションを起こす。
  3. mainの値 IO () を返す。

haskellの方は少し怪しいけど、確かに実質的にはCと同じだね。
先ほどCでは全て副作用だと書いたけれど、実質的に副作用を無視した書き方もできる。
haskellも、全く副作用がないけれども、実際は副作用があるかのように書かれる。

こうやって比較すると、haskellの方が変に見えて使いづらそうに思える。
でも、ソースコードを比較して欲しい。haskellの方が簡潔に書かれているよね。
動きは大体同じだと思えば、構造を見るにはhaskellの方が見通しが良いんだ。

さて、今はこれ以上突っ込んだ話が出来ないので、次は合流ポイントである入出力へ進んでね。

タグ:

+ タグ編集
  • タグ:
最終更新:2007年09月28日 14:11