プログラムの意味が変わる危険性

 ここまでは,領域漏れを解決するための手段として,seq関数や$!演算子などの関数や,バン!パターンのような糖衣構文を使って正格評価版の関数を提供し,それを利用するという方法を紹介しました。しかし,このような方法は万能ではありません。

 まず,正格評価はプログラムの意味を変えてしまう可能性があります。このことを理解するために,第8回で説明した正格と非正格の意味を思い出してみましょう。例外が発生する状態や,計算が終了しない状態をまとめて⊥と呼びます。そして,値が⊥であるときにその答えも⊥になるような式のことを正格であると呼び,値が⊥であってもその答えが⊥にならないような式を非正格と呼びます。

 遅延評価のもとでは,値が⊥であっても答えが⊥になるとは限りません。

nonuseX x = 1

first x y = x

 nonuseX関数の定義では引数xは利用されないため,nonuseX関数を⊥に適用してもその答えは⊥にはなりません。またfirst関数では引数xと引数yのうち引数xだけを結果として返すため,xが⊥である場合にはその答えは⊥になりますが,xは⊥ではなくyだけが⊥である場合にはその答えは⊥になりません。このように遅延評価の下では非正格であることが許されます。

 一方,正格評価では値が先に評価されるため,非正格な定義をすることはできず,値が⊥である場合には必ず答えも⊥になります。

 このように遅延評価するよう定義されたプログラムと,正格評価するよう定義されたプログラムではプログラムの意味そのものが異なる場合があります。その結果,遅延評価するよう定義されたプログラムをやみくもに正格評価に書き換えると,「遅延評価のもとでは何も問題のなかったプログラムで例外が発生するようになったり,遅延評価では終了する計算が終了しなくなったりしてしまう」といった問題が発生する危険性があります。

 このため,プログラムを正格評価するように書き換える場合にはテストが重要になります。第17回で説明したQuickCheckや第16回で説明したHUnitなどを使ってテストを行い,元のプログラムの定義と意味が変わらないか,変更が許容範囲内あることを確認するようにしてください。

 まだ問題点があります。正格評価を使って効率的なプログラムを書くには,それなりの注意が必要である点です。

 一例を挙げると,foldl関数の正格評価版であるfoldl'関数によって領域漏れの問題を解決できるのは,foldl'関数に渡される関数fが,「f a x」を簡約した時点で完全に評価される関数である場合だけです。こうしたfには「+演算子」などがあります。

 fとして「++演算子」をfoldl'に渡す場合を考えてみましょう。「(++) a x」の結果はリストになります。「(++) a x」の結果が空リストになる特別なケースを除き,WHNFへの簡約でリストを完全に評価することはできません。したがって,評価を保留した状態がサンクとして残ってしまい,領域漏れの問題を解決できなくなります。この問題を解決するには,foldlやfoldl'を使うのではなく,関数内で再帰を直接行って値を正格評価するようにプログラムを書き換える必要があります。

 また,関数内で値を正格評価すると,プログラムの性能が低下する可能性があります。同じ値を何回も評価したり,不必要な値が評価されたりする場合があるからです。特にdeepseq関数を誤って使用した場合には,この問題は致命的です。例を見てみましょう。

foldl's_Wrong_deepseq           :: (NFData b) =>
                                        (a -> b -> a) -> a -> [b] -> a
foldl's_Wrong_deepseq f a []     = a
foldl's_Wrong_deepseq f a (x:xs) = let a' = f a x in
                      xs `deepseq`
                      a' `seq` foldl's_Wrong_deepseq f a' xs

 foldl's_Wrong_deepseq関数では,foldl's_Wrong_deepseq関数が再帰的に呼び出されるたびにdeepseq関数が呼ばれ,xsで参照するリストの要素すべてが正格評価されます。遅延評価だとxsのうち必要な部分だけが評価されますが,正格評価ではxs全体が毎回評価されるため,とても効率が悪くなります。