Haskellの評価戦略とfoldl/foldr (2)

前の記事 d:id:linglang:20110909:1315561320 で、

  foldl (<+>) (z<+>a0) (a1:[a2,a3,a4,a5,a6])

という式を評価するとき、2通りの簡約(計算)順序があることを示しました。

  foldl (<+>) b0 (a1:[a2,a3,a4,a5,a6])           {- 内部簡約 -}
  foldl (<+>) ((z<+>a0)<+>a1) (a2:[a3,a4,a5,a6]) {- 最外最左簡約 -}

Haskellでfoldlを評価する

今度は、Haskellの評価戦略である、最外最左簡約でfoldlを評価してみます。
一番外側から順に展開していくので...

  result =
    foldl (<+>) z (a0:[a1,a2,a3,a4,a5,a6]) =
    foldl (<+>) (z<+>a0) (a1:[a2,a3,a4,a5,a6]) =
    foldl (<+>) ((z<+>a0)<+>a1) (a2:[a3,a4,a5,a6]) =
    foldl (<+>) (((z<+>a0)<+>a1)<+>a2) (a3:[a4,a5,a6]) =
    foldl (<+>) ((((z<+>a0)<+>a1)<+>a2)<+>a3) (a4:[a5,a6]) =
    foldl (<+>) (((((z<+>a0)<+>a1)<+>a2)<+>a3)<+>a4) (a5:[a6]) =
    foldl (<+>) ((((((z<+>a0)<+>a1)<+>a2)<+>a3)<+>a4)<+>a5) (a6:[]) =
    foldl (<+>) (((((((z<+>a0)<+>a1)<+>a2)<+>a3)<+>a4)<+>a5)<+>a6) [] =
    (((((((z<+>a0)<+>a1)<+>a2)<+>a3)<+>a4)<+>a5)<+>a6)

となります。ここまで来ると、あとは一番内側しか簡約できる候補がないので、

  result =
    (((((((z<+>a0)<+>a1)<+>a2)<+>a3)<+>a4)<+>a5)<+>a6) =
    ((((((b0<+>a1)<+>a2)<+>a3)<+>a4)<+>a5)<+>a6) =
    (((((b1<+>a2)<+>a3)<+>a4)<+>a5)<+>a6) =
    ((((b2<+>a3)<+>a4)<+>a5)<+>a6) =
    (((b3<+>a4)<+>a5)<+>a6) =
    ((b4<+>a5)<+>a6) =
    (b5<+>a6) = b6

という風に式が消化されていきます。
せっかく末尾再帰で記述したのに、なんだか内部簡約の時のfoldrと同じような感じになってしまいました。

これは、場合によっては嬉しくないので、Haskellの標準仕様(Haskell2010)には、内部簡約と同じ順序で計算を行う foldl' という関数が用意されています。

Haskellの評価戦略とfoldlはあまり相性がよくない

さて、(今更ですが)なんで私がこんな記事を書き出したのかというと、[twitter:@kazu_yamamoto] さんとの以下のような呟きが発端です。

ここで、[twitter:@kazu_yamamoto]さんが例に出されている

  foldl (&&) True [False,(trace "Haha!" True),True,True]

という式を同じように評価してみます。
最外最左簡約ですので、

  foldl (&&) True [False,(trace "Haha!" True),True,True] =
    foldl (&&) True (False:[(trace "Haha!" True),True,True]) =
    foldl (&&) (True&&False) ((trace "Haha!" True):[True,True]) =
    foldl (&&) ((True&&False)&&(trace "Haha!" True)) (True:[True]) =
    foldl (&&) (((True&&False)&&(trace "Haha!" True))&&True) (True:[]) =
    foldl (&&) ((((True&&False)&&(trace "Haha!" True))&&True)&&True) [] =
    ((((True&&False)&&(trace "Haha!" True))&&True)&&True) =
    (((False&&(trace "Haha!" True))&&True)&&True) = {- 注:(trace ...)は評価されない -}
    ((False&&True)&&True) = 
    (False&&True) = False

という簡約順序になります。論理積の途中にFalseが混じっているので、評価を打ち切ってほしいところですが、

  • 最外最左簡約によって畳み込みが全て式に展開されてしまう
  • foldl が演算を左結合で繋げるので、演算を途中で打ち切れない

ために、期待に沿わない結果となってしまいました。

ここでfoldrの出番

実は、上の式でfoldlの代わりにfoldrを使うと、あっけなく計算が終わります。

  foldr (&&) True [False,(trace "Haha!" True),True,True] =
    foldr (&&) True (False:[(trace "Haha!" True),True,True]) =
    False && (foldr (&&) True [(trace "Haha" True),True,True]) =
    False {- False && _ = False なので! -}

こちらの方が、元々の期待に沿った動作なのではないでしょうか?