Old/sampou.org/Lazy
Lazy
Lazy Evaluation、怠け者評価、遅延評価の話題
- Lazy Evaluation 怠け者評価(遅延評価)って?
- Haskell の Lazy Evaluation を追いかける
- ちょっといたずら
- もっといたずら
- 究極(?)のいたずら (2004/11/12)
- samefringe problem
Lazy Evaluation 怠け者評価(遅延評価)って?
子供の頃(今でも?)、「他人にやれと言われる前に自分から、さっさとやりな さい。」などと言われませんでしたか?一般社会では、普通に会社や組織に所 属して他人に雇用されているときには、これが常識でしょう。
コンピュータプログラムの世界でも同じで、計算はできるだけ前もって済ませ ておくというのが普通らしいです。関数に渡すにもまず、ちゃんと値を計算し て渡す。根がまじめで人がいいので、渡した値が実は使われないかもしれない なんて、疑ったりはしないのね。
でもね、Haskellでは、評価は必要になって、尻をたたかれないかぎり、やり ません。前もってやっておくなんて「信〜じられな〜い」。これをLazy Evaluationといったりします。
いわれて始めて仕事を始めるというやつですね。しかも、いわれた分だけしか やらないというやつですね。
Haskellでプログラムを書き、それを走らせることができています。というこ とは、どこかで、最初に要求を出すやつがいて、そいつが要求を出して、計算 がはじまり、それが連鎖して、計算が継続され、最初の要求に答えたら計算が 終るということがおこっていると考えられますね。
Haskell の Lazy Evaluation を追いかける
Haskellのプログラムは、どのような契機で評価が開始され、どのように評価 が進み、どこまで評価が到達するのでしょう?
単純な計算で、遅延評価を追いかけてみましょう。
Hugs の対話型インタープリタを起動しましょう。
% hugs
Prelude>
対話型のインタープリタにプロンプトが出たましたね。このとき、インタープ リタは何か要求されるのを待っている状態です。さて、今、1 + 1 の結果が知 りたいとしましょう。
Prelude> 1 + 1
2
Prelude>
答の 2 が表示されましたね。なにが起こったのか詳細に考えていきましょう。
- 1 + 1 とタイプして、リターンキーを押した
これは、インタープリタに対する「1 + 1 の結果を印字(PRINT)せよ」という 要求です。処理系は、この要求を理解するために、準備処理をします。いまは、 この準備処理については詳しく考えません(要するに、1 + 2 というデータを、 内部的なプログラムの表現に変換します。その際、入力が Haskell のプログ ラムとして正しいかどうかをチェックします)。
次に、インタープリタは
- 印字ルーチン print に入力渡して、結果を印字するように要求
で print は、文字列を出力するルーチン putStr を使って、印字を実行する のですが、putStr は文字列しか扱えません。文字列を完全に使える状態にし て出力がおこなわれます。データを文字列に変換する関数は show です。
show (1 + 1)
を「評価をする」というのが出発点です。
一般には Int型に対する show の定義はちょっと複雑ですが、以下のような定 義と同等と、今は、考えてください(ここの説明では、 1 + 2 の結果がInt 型 であることを暗黙の前提としています)。
show = \ i -> case i of
0 -> "0"
1 -> "1"
2 -> "2"
...
さて、
show (1 + 1)
の評価ですが、どこまで評価するかというと、 WHNF になったら評価がおわり ます。WHNF というのは、乱暴にいうと一番外側(の一番左)にデータコンスト ラクタが出てきた形、または、 λが出てきた形です。
さて、show (1 + 1) は WHNF でしょうか?違いますね。関数適用ですね。関 数適用を評価するには、まず関数部分を評価します。そして出てきた、λ式の body 部にある仮引数を、実引数で置き換えます。
show を評価
==> \ i -> case i of
0 -> "0"
1 -> "1"
2 -> "2"
...
いちばん外の左に、λがでてきましたね。ですから、show の評価はこれでお しまい。出てきた、λ式の body 部にある仮引数を、実引数で置き換えます。
仮引数を実引数でおきかえる
==> case (1 + 1) of
0 -> "0"
1 -> "1"
2 -> "2"
...
で、ここで、case 式の簡約ですが、
case expr of
pat1 -> expr1
pat2 -> expr2
...
とある場合、この式は pat_n で照合ができるようになるまで、 expr を評価 し、適合したパターンの後にある式に簡約されます。もし、expr が pat_2 と 適合したら、この case 式全体は、expr2 に簡約されます。
そこで、次に、1 + 1 の評価を行います。
Int 型に対する + は通常、組込みです。 (x + y)という式を評価すると + は x 、y を共に評価してから、足し算をして、その結果を返します。
仮想的には以下のように定義されていると思ってください。
(+) :: Int -> Int -> Int
(+) = \ i -> \ j -> case i of
0 -> j
1 -> case j of
0 -> 1
1 -> 2
...
...
というわけで、1 + 1 の評価結果は 2 です。さて、show の計算のとことで、 (1 + 1) が評価されて 2 になったので show のなかの case 式のパターンマッチ
2 → "2"
によって、show (1 + 1) の値として、“2” が選択されます。これが putStr に渡って、2 が印字されるというわけです。
これで、インタープリタが 1 + 1 を評価する時の動きを追いかけられました
要点は、
- 最初の部分は特別だか、それ以外では評価は WHNF が要求されると行なわれる
- WHNF を要求するのは、case 式のパターンマッチだ
- Lazy Evaluation をまじめに追跡するのは難しい
ということです。
ちょっといたずら
インタープリタに、1 + (error “hoge”) というのを評価させてみましょう。 (Hugs のシステムには評価のようすをトレースする機能がないので、手でなに かに書きながら追い掛けます。)
Prelude> 1 + (error "hoge")
Program error: hoge
error “hoge” は評価されるとエラーを起します。これはどの段階でエラーを起したのでしょう。これは、
show (1 + (error "hoge"))
==> (\ i -> case i of
0 -> "0"
1 -> "1"
...)
(1 + (error "hoge"))
==> case (1 + (error "hoge")) of
0 -> "0"
1 -> "1"
...
パターンマッチのために、1 + (error “hoge”) を簡約
==> (\ i -> \ j -> case i of
0 -> j
1 -> case j of
0 -> 1
1 -> 2
...
...
) 1 (error "hoge")
==> case 1 of
0 -> j
1 -> case (error "hoge") of
0 -> 1
1 -> 2
...
...
最初のパターンマッチを行うために、1 が評価され、パターンマッチの結果
==> case (error "hoge") of
0 -> 1
1 -> 2
...
となる。このパターンマッチを行うために、error “hoge” が評価するところ でエラーが起きます。
もっといたずら
上のエラーの契機は、show (1 + (error “hoge”)) の評価を行うときに (1 + (error “hoge”) の評価を行う際に、ありました。 + が引数を2つとも必ず評 価する関数だからですね。
では、+ のかわりに、次のように定義された k という関数をつかってみましょう。
k :: Int -> Int -> Int
k = \ x -> \ y -> x + 1
こんなふうに定義して、hoge.hs にセーブしたものを読み込んで、 1 `k` (error “hoge”) を評価してみましょう。
Prelude> :load hoge.hs
Main> 1 `k` (error "hoge")
2
今度はエラーが起りませんね。つまり、error “hoge” は評価されていないわ けです。追いかけてみましょう。
show (1 `k` (error "hoge"))
==> (\ i -> case i of
0 -> "0"
1 -> "1"
2 -> "2"
...
) (1 `k` (error "hoge"))
==> case (1 `k` (error "hoge")) of
0 -> "0"
1 -> "1"
2 -> "2"
...
パターンマッチのために、1 `k` (error “hoge”) を評価
==> (\ x -> y -> x + 1) 1 (error "hoge")
==> 1 + 1
==> 2
というわけで、(error “hoge”)は評価されなかったわけで、エラーは起きない というわけですね。
究極(?)のいたずら (2004/11/12)
対話型のインタープリタ内で、プロンプトに対して式 exp を入力すると、そ の式の入力の値を印字しようとして、評価が進むといいましたよね。印字のルー チン (print)内で、show exp が返す値を完全に評価された文字列にしようと することで、評価が進みます。では、show が完全な文字列をつくるのに exp を評価しなくてもいいことになっていればどうでしょう。ちょっと解りにくい ですが、とりあえず実験してみましょう。
hugs98-Nov2003 をソースからデフォルトの設定のまま(すなわち、configure スクリプトをオプションなしで)インストールしたと仮定して説明します。こ の場合、/usr/local/lib/hugs/libraries/Hugs/ に、Prelude.hs がインストー ルされています。これの 299行目あたり
instance Show Int where
showsPrec = primShowsInt
となっているところを
instance Show Int where
-- showsPrec = primShowsInt
showsPrec _ _ _ = "<Int>"
と書き換えてください。これは何をやったかというと、Int 型の値を文字列 (String型)の値に変換する関数を定義を、与えられた Int 型の値にかかわら ず(つまり評価することなく)単に、“<Int>”とという文字列を返すように変更 を施しています。
さて、これで、hugs インタープリタを起動して、実験です。まず、プロンプ トから length [1,2,3] を入力してみましょう。まずは、予想をして、何が印 字されるでしょう。
length [1,2,4] を評価すると 3 になるはずですよね。
Prelude> length [1,2,4]
"<Int>"
length の型は、[a] -> Int なので、lenght [1,2,4] の値は Int 型です。先 程の変更が効果を発揮して、<Int> が印字されています。
では、length [1,2,4] は評価されたのでしょうか。これだけではわかりませ んね。どうしたら解るでしょう? length [1,2,4] が評価されたとすると、 length の定義から、引数のリストを最後まで辿って、要素の数を特定してい るはずですね。そこで最後まで辿りきれない無限リスト[1..] を length に渡 してみましょう。
Prelude> length [1..]
<Int>
ちゃんとプロンプトが返ってきますね。つまり、lenght [1..] は評価されて いないのです。
なぜでしょう。それは、Int型に対する show 関数がその引数の値を使ってい ないからです。すなわち、Int型を印字するのに、Int型の値を必要としないよ うに書換えてしまったからです。length [1..] の値が Int 型であることを知 るのに length [1..] を評価する必要がないことを思い出してください。
length [1,2,4] + error “hoge” をプロンプトから入力してみましょう。
Prelude> length [1,2,4] + error "hoge"
<Int>
これもエラーは発生しません。さらに、
Prelude> error "hoge" :: Int
<Int>
これでさえ大丈夫です。もちろん、ちゃんと length [1..] を評価するとそれ は停止しません。確かめてみましょう。
Prelude> take (length [1,2,4]) "The Haskell Language"
"The"
Prelude> take (length [1..]) "The Haskell Language"
"{Interrupted!}
“{Interrupted!} は実行を Ctrl-C で止めたときに出たものです。 length [1,2,4] + error”hoge" はちゃんと評価すると、もちろんエラーになります。
Prelude> take (length [1,2,4] + error "hoge") "The Haskell Language"
"
Program error: hoge
究極の実験の後始末を忘れないようにしましょう。無茶な変更を加えたところを元にもどしておきましょう。忘れると変なところで、ワケワカなことになりますからね。
samefringe problem
Haskell なら単に fringe を比べればいいだけだよーん.
data Tree a = Leaf a | Branch [Tree a]
fringe :: Tree a -> [a]
fringe (Leaf x) = [x]
fringe (Branch cs) = foldr ((++) . fringe) [] cs
samefringe :: Eq a => Tree a -> Tree a -> Bool
samefringe t u = fringe t == fringe u
Last modified : 2006/11/22 22:35:38 JST