5

Consider these various attempts at something that works like last:

Prelude> import Data.Foldable
Prelude Data.Foldable> foldr const undefined (reverse [1,2,3])
3
Prelude Data.Foldable> foldr' const undefined (reverse [1,2,3])
3
Prelude Data.Foldable> foldl (flip const) undefined [1,2,3]
3
Prelude Data.Foldable> foldl' (flip const) undefined [1,2,3]
*** Exception: Prelude.undefined
CallStack (from HasCallStack):
  error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err
  undefined, called at <interactive>:5:21 in interactive:Ghci4

It makes sense to me that foldl and foldr both work, since they aren't strict in their accumulator, and it makes sense to me that foldl' doesn't, since it is. But why does foldr' work? Isn't it supposed to be strict in its accumulator too?

1 Answers1

3

For reference, the instance Foldable [] overrides foldr, foldl, foldl', but not foldr' (source):

instance Foldable [] where
    elem    = List.elem
    foldl   = List.foldl
    foldl'  = List.foldl'
    foldl1  = List.foldl1
    foldr   = List.foldr
    {- ... -}

foldr' is defined by default as (source):

foldr' :: (a -> b -> b) -> b -> t a -> b
foldr' f z0 xs = foldl f' id xs z0
  where f' k x z = k $! f x z

Note that there is only a strictness annotation on the result of f. So the initial accumulator is not forced.

This suggests a different implementation which does force the accumulator:

foldr'' :: Foldable t => (a -> b -> b) -> b -> t a -> b
foldr'' f = foldr (\x z -> f x $! z)

(Edited: the previous version was specialized to lists.)

I have no idea why one was chosen over the other. Probably an oversight, and it would be more consistent for foldr' to not use the default implementation in the Foldable [] instance.


As an aside, the default definition of foldl' is also different from the list one in the same way:

-- Default (class Foldable t where ...)
foldl' :: (b -> a -> b) -> b -> t a -> b
foldl' f z0 xs = foldr f' id xs z0
  where f' x k z = k $! f z x

-- List implementation
foldl'           :: forall a b . (b -> a -> b) -> b -> [a] -> b
foldl' k z0 xs =
  foldr (\(v::a) (fn::b->b) -> oneShot (\(z::b) -> z `seq` fn (k z v))) (id :: b -> b) xs z0
Li-yao Xia
  • 31,896
  • 2
  • 33
  • 56
  • 2
    Your definition is specialized to lists, original one is for generic Foldable (with e.g. only lazy `foldr` defined). One could have `foldl f' id xs $! z0`, I guess. (I haven't try if it makes things "correctly" stricter). – phadej Aug 06 '20 at 16:16
  • That's a fair observation. Actually, out of the four `fold[lr]'?` variants, `foldr'` is the only one `Foldable []` does not override, leading to this inconsistent behavior. – Li-yao Xia Aug 06 '20 at 16:24
  • 2
    Oh, that's weird indeed. (Given that Foldable is huge dictionary anyway, having one more method would make sense, especially in this case for Seq or SnocList). – phadej Aug 06 '20 at 16:26
  • I just changed the second definition to not be specific to lists. – Li-yao Xia Aug 06 '20 at 16:32
  • 2
    For the definitions of `foldl'` and `foldr'` on generic `Foldable`s, it makes more sense for `foldl'` to be defined in terms of `foldr`, and for `foldr'` to be defined in terms of `foldl`. This is because you generally want `foldl'` to work something like a tail-recursive strict function (like for sums or whatever), and you cannot define that (on cons-lists) in terms of `foldl`. `foldr'` would function the same for snoc-lists. The generic definition on lists also benefits from the specialisation. There is no "good" version of `foldr'`, which is probably why a special version is not included. – oisdk Aug 06 '20 at 19:05
  • To be clear, the fact that the overridden `List.foldl'` has a different strictness is probably is probably an oversight. I'm just explaining why some are overridden and some aren't. – oisdk Aug 06 '20 at 19:26
  • 3
    Hysterical raisins. This inconsistency is one of Joachim Breitner's few mistakes. When he came up with how brilliant way to make `foldl` fuse nicely, he applied the same trick to `foldl'`. Unfortunately, when he did so, he accidentally made the function stricter. – dfeuer Aug 06 '20 at 22:19