これは、Haskellスペースリーク Advent Calendar 2015の7日目の記事です。
僕は Haskell で主に IO なコードを書いているからか、あまりスペースリークを起こしたことがない。これまで起こした唯一のスペースリークは、サーバプログラムの中の以下のようなコードだった。
atomicModifyIORef ref (\_ -> (tmstr, ()))
これは、時間を文字列に変換して IORef にキャッシュさせるコードだ。あるスレッドが、毎秒このコードを呼び出す。不思議なことに、クライアントからのアクセスがあるとスペースリークにはならないのだが、長時間アクセスがないとサーバのプロセスが太っていた。
このリークは、結果を利用していないために起こる。そもそも、このコードは古い値を利用せずに、単に新しい値で置き換えているだけだから、writeIORef で十分だ。
writeIORef ref tmstr
では、カウンターのように古い値を使う場合はどうすればいいだろう?
atomicModifyIORef ref (\i -> (i+1, ()))
このスペースリークをなくすための定石は、結果を評価してやることである。
x <- atomicModifyIORef ref (\i -> (i+1, ())) x `seq` return ()
BanPatterns を使うと、すこしスッキリする。
!_ <- atomicModifyIORef ref (\i -> (i+1, ()))
一番よいのは、CAS で値を置き換えるときは遅延評価で、CAS が成功し後に正格評価する atomicModifyIORef' を使うことだ。
atomicModifyIORef' ref (\i -> (i+1, ()))
おまけ
昔この話を Monad.Reader 19に書いたところ、atomicModifyIORef のマニュアルに注意書きが足された。:-)
atomicModifyIORef' は、並行 Haskell の奥義である。詳しくは「Haskellによる並列・並行プログラミング」を読んでほしい。