Haskellで同じ名前が常に同じ値を返すとは限らないという話

モナドって結局何なのよ? — join to Monad v0.1.1 documentationを読んでいて、そこの指摘が「ああ確かにこれは他の言語から来た人がつまずきやすい点だな」と思ったのでそこだけ切り出して詳しく書いてみる。まずは下のコードの穴埋めをしてみよう。

$ ghci
Prelude> import ******
Prelude ******> let f x = *****
Prelude ******> (f 0) * 1
2092838931
Prelude ******> (f 0) / 1
0.9872770354820595
Prelude ******> (f 0) * 1 == (f 0) / 1
True

Haskellの関数は純粋だ、同じ関数に同じ引数を渡せば常に同じ値が返る」うんうん、それには同意する。だがしかし、それでもなお(f x)が異なる値に評価されるケースがある。それは文脈に依存する。もちろん、letやwhereを使ってfやxの指す値を変えれば(f x)の値も変わる。そういうことを言っているんじゃないよ。これはC++Javaでは起こらないから、つまずきどころかと思う。

Prelude> import Random
Prelude Random> let f x = (fst (random (mkStdGen x)))
Prelude Random> (f 0) * 1
2092838931
Prelude Random> (f 0) / 1
0.9872770354820595
Prelude Random> (f 0) * 1 == (f 0) / 1
True

C++Javaでは、引数の型が異なる関数をいくつも定義してオーバーロードできる。しかしHaskellではそれに加えて「その式を評価した結果の型」が異なる定義をいくつもオーバーロードできる。関数である必要すらない。ある名前xが指す値がなんであるかは、ユーザからは見えにくい型推論の結果によって決まり、そのxの値はかなり離れた位置のコードからでも影響を受ける。コード上で局在していない。

class Foo a where
    x :: a

instance Foo Int where
    x = 0

instance Foo Integer where
    x = 1

instance Foo Float where
    x = 2

instance Foo Double where
    x = 3

main = do
  print $ x + 0 + 0 + 0 * (1 :: Int)
  print $ x + 0 + 0 + 0 * (1 :: Integer)
  print $ x + 0 + 0 + 0 * (1 :: Float)
  print $ x + 0 + 0 + 0 * (1 :: Double)

この例では「型の違う0を足しているだけ」ということがわかるようにすぐそばで明示的に型を宣言しているが、推論によって演算子をいくつもまたいで影響を与えていることがわかるかと思う。

そんなわけで、(return x)が何を返すかはこのコードだけからでは分からないし、(return x) >>= ... >>= f というコードのfの定義を変えると(return x)の値が変わるかもしれない、という可能性に関しては念頭におく必要がある。つまずきポイントだ。