Can `foldr` and `foldl` be defined in terms of each other?

对着背影说爱祢 提交于 2019-12-08 14:24:20

问题


Can foldr and foldl be defined in terms of each other?

Programming in Haskell by Hutton says

What do we need to define manually? The minimal complete definition for an instance of the Foldable class is to define either foldMap or foldr, as all other functions in the class can be derived from either of these two using the default definitions and the instance for lists.

So how can foldl be defined in terms of foldr?

Can foldr be defined in terms of foldl, so that we can define a Foldable type by defining foldl?

Why is it that in Foldable, fold is defined in terms of foldMap which is defined in terms of foldr, while in list foldable, some specializations of fold are defined in terms of foldl as:

maximum :: Ord a => [a] -> a
maximum = foldl max

minimum :: Ord a => [a] -> a
minimum = foldl min

sum :: Num a => [a] -> a
sum = foldl (+) 0

product :: Num a => [a] -> a
product = foldl (*) 1

? Can they be rewritten as

maximum :: Ord a => [a] -> a
maximum = foldr max

minimum :: Ord a => [a] -> a
minimum = foldr min

sum :: Num a => [a] -> a
sum = foldr (+) 0

product :: Num a => [a] -> a
product = foldr (*) 1

Thanks.


回答1:


In general, neither foldr nor foldl can be implemented in terms of each other. The core operation of Foldable is foldMap, from which all the other operations may be derived. Neither foldr nor foldl are enough. However, the difference only shines through in the case of infinite or (partially) undefined structures, so there's a tendency to gloss over this fact.

@DamianLattenero has shown the "implementations" of foldl and foldr in terms of one another:

foldl' c = foldr (flip c)
foldr' c = foldl (flip c)

But they do not always have the correct behavior. Consider lists. Then, foldr (:) [] xs = xs for all xs :: [a]. However, foldr' (:) [] /= xs for all xs, because foldr' (:) [] xs = foldl (flip (:)) n xs, and foldl (in the case of lists) has to walk the entire spine of the list before it can produce an output. But, if xs is infinite, foldl can't walk the entire infinite list, so foldr' (:) [] xs loops forever for infinite xs, while foldr (:) [] xs just produces xs. foldl' = foldl as desired, however. Essentially, for [], foldr is "natural" and foldl is "unnatural". Implementing foldl with foldr works because you're just losing "naturalness", but implementing foldr in terms of foldl doesn't work, because you cannot recover that "natural" behavior.

On the flipside, consider

data Tsil a = Lin | Snoc (Tsil a) a
-- backwards version of data [a] = [] | (:) a [a]

In this case, foldl is natural:

foldl c n Lin = n
foldl c n (Snoc xs x) = c (foldl c n xs) x

And foldr is unnatural:

foldr c = foldl (flip c)

Now, foldl has the good, "productive" behavior on infinite/partially undefined Tsils, while foldr does not. Implementing foldr in terms of foldl works (as I just did above), but you cannot implement foldl in terms of foldr, because you cannot recover that productivity.

foldMap avoids this issue. For []:

foldMap f [] = mempty
foldMap f (x : xs) = f x <> foldMap f xs
-- foldMap f = foldr (\x r -> f x <> r) mempty

And for Tsil:

foldMap f Lin = mempty
foldMap f (Snoc xs x) = foldMap f xs <> f x
-- foldMap f = foldl (\r x -> r <> f x) mempty

Now,

instance Semigroup [a] where
  [] <> ys = ys
  (x : xs) <> ys = x : (xs <> ys)
  -- (<>) = (++)
instance Monoid [a] where mempty = []
instance Semigroup (Tsil a) where
  ys <> Lin = ys
  ys <> (Snoc xs x) = Snoc (ys <> xs) x
instance Monoid (Tsil a) where mempty = Lin

And we have

foldMap (: []) xs = xs -- even for infinite xs
foldMap (Snoc Lin) xs = xs -- even for infinite xs

Implementations for foldl and foldr are actually given in the documentation

foldr f z t = appEndo (foldMap (Endo . f) t ) z
foldl f z t = appEndo (getDual (foldMap (Dual . Endo . flip f) t)) z

f is used to turn each a in the t a into a b -> b (Endo b), and then all the b -> bs are composed together (foldr does it one way, while foldl composes them backwards with Dual (Endo b)) and the final b -> b is then applied to the initial value z :: b.

foldr is replaced with foldl in specializations sum, minimum, etc. in the instance Foldable [], for performance reasons. The idea is that you can't take the sum of an infinite list anyway (this assumption is false, but it's generally true enough), so we don't need foldr to handle it. Using foldl is, in some cases, more performant than foldr, so foldr is changed to foldl. I would expect, for Tsil, that foldr is sometimes more performant than foldl, and therefore sum, minimum, etc. can be reimplemented in terms of foldr, instead of fold in order to get that performance improvement. Note that the documentation says that sum, minimum, etc. should be equivalent to the forms using foldMap/fold, but may be less defined, which is exactly what would happen.


Bit of an appendix, but I think it's worth noticing that:

genFoldr c n [] = n; genFoldr c n (x : xs) = c x (genFoldr c n xs)
instance Foldable [] where
  foldl c = genFoldr (flip c)
  foldr c = foldl (flip c)
-- similarly for Tsil

is actually a valid, lawful Foldable instance, where both foldr and foldl are unnatural and neither can handle infinite structures (foldMap is defaulted in terms of foldr, and thus won't handle infinite lists either). In this case, foldr and foldl can be written in terms of each other (foldl c = foldr (flip c), though it is implemented with genFoldr). However, this instance is undesirable, because we would really like a foldr that can handle infinite lists, so we instead implement

instance Foldable [] where
  foldr = genFoldr
  foldl c = foldr (flip c)

where the equality foldr c = foldl (flip c) no longer holds.




回答2:


In the case of lists: foldl can be defined in terms of foldr but not vice-versa.

foldl f a l = foldr (\b e c -> e (f c b)) id l a

For other types which implement Foldable: the opposite may be true.




回答3:


Here's a type for which neither foldl nor foldr can be implemented in terms of the other:

import Data.Functor.Reverse
import Data.Monoid

data DL a = DL [a] (Reverse [] a)
  deriving Foldable

The Foldable implementation looks like

instance Foldable DL where
  foldMap f (DL front rear) = foldMap f front <> foldMap f rear

Inlining the Foldable instance for Reverse [], and adding the corresponding foldr and foldl,

  foldMap f (DL front rear) = foldMap f front <> getDual (foldMap (Dual . f) (getReverse rear))
  foldr c n (DL xs (Reverse ys)) =
    foldr c (foldl (flip c) n ys) xs
  foldl f b (DL xs (Reverse ys)) =
    foldr (flip f) (foldl f b xs) ys

If the front list is infinite, then foldr defined using foldl won't work. If the rear list is infinite, then foldl defined using foldr won't work.




回答4:


Edit 2:

There is another way also that satisfy (based on this article) for foldl:

foldl f a list = (foldr construct (\acc -> acc) list) a
  where
    construct x r = \acc -> r (f acc x)

Edit 1

Flipping the arguments of the function will not create a same foldr/foldl, meaning this examples does not satisfy the equality of foldr-foldl:

foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b

and foldl in terms of foldr:

foldl' :: Foldable t => (b -> a -> b) -> b -> t a -> b
foldl' f b = foldr (flip f) b 

and foldr:

foldr' :: Foldable t => (a -> b -> b) -> b -> t a -> b
foldr' f b = foldl (flip f) b 

The converse is not true, since foldr may work on infinite lists, which foldl variants never can do. However, for finite lists, foldr can also be written in terms of foldl although losing laziness in the process. (for more check here)

Ando also not satisfy this examples:

foldr (-) 2 [8,10] = 8 - (10 - 2) == 0

foldl (flip (-)) 2 [8,10] = (flip (-) (flip (-) 2 8) 10) == 4


来源:https://stackoverflow.com/questions/57260253/can-foldr-and-foldl-be-defined-in-terms-of-each-other

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!