この記事では、Haskell における DeepSeq を使用した完全評価の方法を紹介します。

振り返り
前回の記事では、seq
関数が最初の引数を弱頭正規形(WHNF)まで評価することについて説明しました。
しかし、どのようにして式を正規形になるまで、つまり完全に評価できるのでしょうか?ここで登場するのが DeepSeq
です。
NFData
Control.DeepSeq
には NFData
という型クラスがあり、これは正規形データを意味します。このパッケージ内には、
既に有効な NFData
のインスタンスとして定義されている多くの事前定義されたデータ型があります。
import Control.DeepSeq
instance NFData Bool
instance NFData Char
instance NFData Maybe
-- etc.
データ型を NFData
型クラスの有効なインスタンスにするには、データ型の引数を完全に評価する rnf
(reduce to normal form) 関数を定義する必要があります。
data PeaNum = Succ PeaNum | Zero deriving Show
instance NFData PeaNum where
rnf Zero = ()
rnf (Succ a) = rnf a
上記の例では、PeaNum
を再帰的に定義し、rnf
を定義することで NFData
のインスタンスにしました。rnf
は Zero
に達するまで再帰的に rnf を適用します。
これにより、PeaNum
を完全に評価できます。別の例を見てみましょう。
data Tree a = Node (Tree a) a (Tree a) | Leaf deriving Show
instance NFData a => NFData (Tree a) where
rnf Leaf = ()
rnf (Node l v r) = rnf l `seq` rnf v `seq` rnf r
上記の例では、seq
関数を上手く使用して左部分木を評価し、その後に v
を評価し、最後に r
を評価します。
deepseq
データ型を NFData
の有効なインスタンスにしたら、deepseq
関数を使用してデータを正規形に変換することができます。
five = Succ $ Succ $ Succ $ Succ $ Succ Zero
-- ghci> :sprint five
-- five = _
-- ghci> deepseq five ()
-- ()
-- ghci> :sprint five
-- five = Succ (Succ (Succ (Succ (Succ Zero))))
seq
関数が WHNF までしか評価しないのに対し(five = Succ _
)、deepseq
関数は正規形まで完全に評価します(five = Succ (Succ (Succ (Succ (Succ Zero))))
)。
なぜ DeepSeq を使うのか
いくつかのケースでは、完全な評価を必要とする場面がいくつかありますが、以下の IO アクションがその一例です。
import System.IO
import Control.DeepSeq
main = do
h <- openFile "f" ReadMode
s <- hGetContents h
s `deepseq` hClose h
return s
上記の IO アクションでは、ファイルを開き、そのファイルの内容を取得してからファイルを閉じます。ファイルを閉じる前に、hGetContents h
を完全に評価して内容を取得したことを確認する必要があります。そうしないと、ファイルを閉じた後に取得できないサンク(遅延評価された式)
が残ってしまいます。このような場合に deepseq
を使用できます。
既に述べたように、Haskell では避けられない場合にのみ厳密評価を使用するべきです。厳密評価は賢明に使用するようにしてください。
クイズ
この記事では、学習した内容を確認するためのクイズを設けます。記事のメイン部分を読んだ後に、ぜひ自分で問題を解いてみることを強くお勧めします。各問題をクリックすると答えが表示されます。
リソース
- Philipp, Hagenlocher. 2020. Haskell for Imperative Programmers #32 - DeepSeq. YouTube.
- nd. Control.DeepSeq. deepseq-1.4.1.1: Deep evaluation of data structures.