Haskellerへの道 #22 - DeepSeq

Last Edited: 7/31/2024

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

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 のインスタンスにしました。rnfZero に達するまで再帰的に 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 では避けられない場合にのみ厳密評価を使用するべきです。厳密評価は賢明に使用するようにしてください。

クイズ

この記事では、学習した内容を確認するためのクイズを設けます。記事のメイン部分を読んだ後に、ぜひ自分で問題を解いてみることを強くお勧めします。各問題をクリックすると答えが表示されます。

リソース