Stricter Haskell

なにこれ

Haskellは素敵な言語だと思うが、デフォルトが遅延評価であることに起因する欠点のせいで魅力を50%くらい損なっているように見える。具体的にはサンク構築/評価のオーバーヘッドとメモリリークで、特にメモリリークの実害は大きい。ならば必要な所以外で評価を遅延させないコーディングをすれば、うっかりメモリリークを作ってしまう確率を減らせるに違いないというのがこの記事の主旨。

既に発生してしまったメモリリークに対処するのは別の問題で、有効な方法も全然違うだろう。

原則

特にそうしない必要のない限り、サンクを作ったらすぐ壊す。つまり、

let x = f y

と書く代わりに、

let !x = f y

と書き、

return (f x)

と書く代わりに

return $! f x

と書く。

評価の責任

基本的に、サンクを作った者がそれを壊す責任を負うことにする。つまり、関数引数は呼び出し側が評価し、関数の結果(に入れ子になった値)は関数側が評価する。

foo :: (Int, Int) -> Maybe Int
foo (arg1, arg2) = Just $! arg2 + 1

bar :: Int -> Int
bar n = fromMaybe 0 $ foo (x, y)
  where
    !x = n * n
    !y = x * x

ただし引数を受け取る側を正格にした方が楽なこともあるので臨機応変に。特に再帰関数の場合など。

loop !x = do
  n <- readLn
  loop (x + n)

また、「作った者が壊す」原則に従うなら構築子に!を付けて正格にする必要はないが、付けた方が後で楽になる場合が多い。

f, g :: Int -> (Int, Int)

-- サンクを返している
f n = (n - 1, n + 1)

-- サンクを明示的に潰す。面倒
g n = (x, y)
  where
    !x = n - 1
    !y = n + 1

-- 正格なデータ構造を使えば…
data Pair a b = Pair !a !b

-- サンクを楽に潰せる
h :: Int -> Pair Int Int
h n = Pair (n - 1) (n + 1)

サンクを返す関数

標準ライブラリを含めてほとんどのHaskell関数は以上の約束に従わないので、サンクを含む値を返してくることがある。良くあるのはIOに関するfmapやap。

do
  x <- negate <$> return 4
  -- ここでxは-4という値ではなく(negate 4)というサンク
  ...

そういう場合は呼び出し元でいちいち潰してやるしかない。

do
  !x <- neagte <$> return 4
  ...

また、一部の関数は潰しにくいサンクを含むデータを返す。筆頭はData.Map.mapだろう。こういうのはそもそも使わずに済ませることができると楽。この場合はData.Map.map fの代わりにData.Map.mapMaybe ((Just$!) . f)を使うという方法がある(これはバッドノウハウの類だろうけど)。

おまけ

レコードからフィールドの値を取り出す際には、パターンマッチを使った方が、フィールド抽出関数を使った場合に比べて潰さなければならないサンクが減ることが多い。しかしレコードのパターンマッチはタイプ量が多くなりがちでつい避けたくなってしまう。そういう場合RecordWildcards拡張を使うとほどよい堕落ができる。

data Record = Record { recFoo :: !Int, recBar :: !Int, recBaz :: !String }

f, g, h :: Record -> (Int, String)
f Record{ recFoo = foo, recBaz = baz } = (foo, baz) -- パターンマッチを使う。面倒
g rec = (foo, baz) -- 抽出関数を使う。サンク潰しが面倒
  where
    !foo = recFoo rec
    !baz = recBaz foo
h Record{..} = (recFoo, recBaz) -- 素敵!

ただしこれは実際に堕落であって、フィールド抽出関数を黙って覆い隠すのでバグの原因になり得る。