ようこそ。睡眠不足なプログラマのチラ裏です。

Haskellのお勉強 その6

遅延評価

Haskellの大きな特徴として、遅延評価を基本原則としているというのがある。
遅延評価とは、評価しなければならない値が存在するとき、実際の計算を値が必要になるまで行わないことを言う。
つまり、遅延評価はトヨタ式の、必要なときに必要なものを必要なだけ作るってゆー
ジャストインタイムのようなものをイメージすると理解しやすいかも。


Haskellの式の評価は、最外簡約でグラフ簡約

関数の適用を定義で置き換えることを簡約という。
簡約には最内簡約と最外簡約がある。


以下のような関数があった場合

square n = n * n


■最内簡約

{-C#やJavaの評価のされ方-}
square (2 + 3)
→ square 55 * 525

■最外簡約

{-Haskellの評価のされ方-}
square (2 + 3)
→ (2 + 3) * (2 + 3)
→ 5 * 525

上記の最外簡約の(2 + 3)が2つに増えていることに着目したとき、
(2 + 3)は2箇所別々に簡約されるだろうか。それとも
引数としては1つであるから、簡約は1度のみになるのだろうか。
Haskellの評価方法は、後者となる。つまり、1つの引数は1度だけ評価される。
このように式を共有しながら簡約する方法をグラフ簡約と言う。
この最外簡約とグラフ簡約の二本柱が、Haskellの遅延評価の礎となっている。


ifを関数として書く

ふつける本に載ってるまんまだが、ifを関数として表現すると下記のようになる。

main = do myIf (True) (putStrLn "then") (putStrLn "else")

myIf :: Bool -> a -> a -> a
myIf True  t e = t
myIf False t e = e

この独自に定義したmyIf関数を使ったプログラムは「"then"」だけを表示する。
したがって、else側のアクションは評価されず遅延されたままとなることがわかる。
このように遅延評価には、評価に必要な式だけが評価されるという特徴がある。

データ構造の遅延評価

Haskellではデータ構造も遅延評価される。
つまり、データ構造も必要最小限の部分のみが作成される。
例えばリストであれば、実際にプログラムで利用される要素のみが作成される。
つまり、あるリストの第1要素しか使わないのであれば、そのリストの第2要素以降は作成されない。

例えば以下のような場合、一見lines csによって
標準入力すべての要素に対してリスト分割されそうに見えるが。
take nがあり、nは10であるから、この場合、lines csでは10行分のリストのみが取得される。

main = do cs <- getContents
          putStr $ firstNLines 10 cs
firstNLines n cs = unlines $ take n $ lines cs


また、下記の場合、リストを末尾まで評価しなければlength関数の戻り値が得られないので
リスト自体はすべて評価されることとなるが、リストの要素は必要ないので、
(1 + 1)、(2 + 2)、(3 + 3)のそれぞれの評価は遅延されたまま、length関数は3を得ることができる。

length [(1 + 1),(2 + 2),(3 + 3)]


遅延評価のメリット/デメリット

■遅延評価のメリット
・不必要な評価を減らすことができる
・無限の長さのリストが扱うことができる
 (終わりの見えない、あるいは終わりがないデータをリストとして扱うことができる)
・インターフェイスが統一できる(すべての操作は「リストに対する処理」とみなせる)

■遅延評価のデメリット
・想定する順番で操作を実行するのが難しい
ソースコードの可読性が低くなりがち
デバッグしにくい(スタックトレースが存在しない)