Why can you reverse list with foldl, but not with foldr in Haskell

前端 未结 6 1820
栀梦
栀梦 2021-02-04 09:49

Why can you reverse a list with the foldl?

reverse\' :: [a] -> [a]
reverse\' xs = foldl (\\acc x-> x : acc) [] xs

But this one gives me a

相关标签:
6条回答
  • 2021-02-04 10:13

    You can use foldr to reverse a list efficiently (well, most of the time in GHC 7.9—it relies on some compiler optimizations), but it's a little weird:

    reverse xs = foldr (\x k -> \acc -> k (x:acc)) id xs []
    

    I wrote an explanation of how this works on the Haskell Wiki.

    0 讨论(0)
  • 2021-02-04 10:15

    A slight but significant generalization of several of these answers is that you can implement foldl with foldr, which I think is a clearer way of explaining what's going on in them:

    myMap :: (a -> b) -> [a] -> [b]
    myMap f = foldr step []
        where step a bs = f a : bs
    
    -- To fold from the left, we:
    --
    -- 1. Map each list element to an *endomorphism* (a function from one
    --    type to itself; in this case, the type is `b`);
    --
    -- 2. Take the "flipped" (left-to-right) composition of these
    --    functions;
    --
    -- 3. Apply the resulting function to the `z` argument.
    --
    myfoldl :: (b -> a -> b) -> b -> [a] -> b
    myfoldl f z as = foldr (flip (.)) id (toEndos f as) z
        where
          toEndos :: (b -> a -> b) -> [a] -> [b -> b]
          toEndos f = myMap (flip f)
    
    myReverse :: [a] -> [a]
    myReverse = myfoldl (flip (:)) []
    

    For more explanation of the ideas here, I'd recommend reading Tom Ellis' "What is foldr made of?" and Brent Yorgey's "foldr is made of monoids".

    0 讨论(0)
  • 2021-02-04 10:18

    Every foldl is a foldr.

    Let's remember the definitions.

    foldr :: (a -> s -> s) -> s -> [a] -> s
    foldr f s []       = s
    foldr f s (a : as) = f a (foldr f s as)
    

    That's the standard issue one-step iterator for lists. I used to get my students to bang on the tables and chant "What do you do with the empty list? What do you do with a : as"? And that's how you figure out what s and f are, respectively.

    If you think about what's happening, you see that foldr effectively computes a big composition of f a functions, then applies that composition to s.

    foldr f s [1, 2, 3]
    = f 1 . f 2 . f 3 . id $ s
    

    Now, let's check out foldl

    foldl :: (t -> a -> t) -> t -> [a] -> t
    foldl g t []       = t
    foldl g t (a : as) = foldl g (g t a) as
    

    That's also a one-step iteration over a list, but with an accumulator which changes as we go. Let's move it last, so that everything to the left of the list argument stays the same.

    flip . foldl :: (t -> a -> t) -> [a] -> t -> t
    flip (foldl g) []       t = t
    flip (foldl g) (a : as) t = flip (foldl g) as (g t a)
    

    Now we can see the one-step iteration if we move the = one place leftward.

    flip . foldl :: (t -> a -> t) -> [a] -> t -> t
    flip (foldl g) []       = \ t -> t
    flip (foldl g) (a : as) = \ t -> flip (foldl g) as (g t a)
    

    In each case, we compute what we would do if we knew the accumulator, abstracted with \ t ->. For [], we would return t. For a : as, we would process the tail with g t a as the accumulator.

    But now we can transform flip (foldl g) into a foldr. Abstract out the recursive call.

    flip . foldl :: (t -> a -> t) -> [a] -> t -> t
    flip (foldl g) []       = \ t -> t
    flip (foldl g) (a : as) = \ t -> s (g t a)
      where s = flip (foldl g) as
    

    And now we're good to turn it into a foldr where type s is instantiated with t -> t.

    flip . foldl :: (t -> a -> t) -> [a] -> t -> t
    flip (foldl g) = foldr (\ a s -> \ t -> s (g t a)) (\ t -> t)
    

    So s says "what as would do with the accumulator" and we give back \ t -> s (g t a) which is "what a : as does with the accumulator". Flip back.

    foldl :: (t -> a -> t) -> t -> [a] -> t
    foldl g = flip (foldr (\ a s -> \ t -> s (g t a)) (\ t -> t))
    

    Eta-expand.

    foldl :: (t -> a -> t) -> t -> [a] -> t
    foldl g t as = flip (foldr (\ a s -> \ t -> s (g t a)) (\ t -> t)) t as
    

    Reduce the flip.

    foldl :: (t -> a -> t) -> t -> [a] -> t
    foldl g t as = foldr (\ a s -> \ t -> s (g t a)) (\ t -> t) as t
    

    So we compute "what we'd do if we knew the accumulator", and then we feed it the initial accumulator.

    It's moderately instructive to golf that down a little. We can get rid of \ t ->.

    foldl :: (t -> a -> t) -> t -> [a] -> t
    foldl g t as = foldr (\ a s -> s . (`g` a)) id as t
    

    Now let me reverse that composition using >>> from Control.Arrow.

    foldl :: (t -> a -> t) -> t -> [a] -> t
    foldl g t as = foldr (\ a s -> (`g` a) >>> s) id as t
    

    That is, foldl computes a big reverse composition. So, for example, given [1,2,3], we get

    foldr (\ a s -> (`g` a) >>> s) id [1,2,3] t
    = ((`g` 1) >>> (`g` 2) >>> (`g` 3) >>> id) t
    

    where the "pipeline" feeds its argument in from the left, so we get

    ((`g` 1) >>> (`g` 2) >>> (`g` 3) >>> id) t
    = ((`g` 2) >>> (`g` 3) >>> id) (g t 1)
    = ((`g` 3) >>> id) (g (g t 1) 2)
    = id (g (g (g t 1) 2) 3)
    = g (g (g t 1) 2) 3
    

    and if you take g = flip (:) and t = [] you get

    flip (:) (flip (:) (flip (:) [] 1) 2) 3
    = flip (:) (flip (:) (1 : []) 2) 3
    = flip (:) (2 : 1 : []) 3
    = 3 : 2 : 1 : []
    = [3, 2, 1]
    

    That is,

    reverse as = foldr (\ a s -> (a :) >>> s) id as []
    

    by instantiating the general transformation of foldl to foldr.

    For mathochists only. Do cabal install newtype and import Data.Monoid, Data.Foldable and Control.Newtype. Add the tragically missing instance:

    instance Newtype (Dual o) o where
      pack = Dual
      unpack = getDual
    

    Observe that, on the one hand, we can implement foldMap by foldr

    foldMap :: Monoid x => (a -> x) -> [a] -> x
    foldMap f = foldr (mappend . f) mempty
    

    but also vice versa

    foldr :: (a -> b -> b) -> b -> [a] -> b
    foldr f = flip (ala' Endo foldMap f)
    

    so that foldr accumulates in the monoid of composing endofunctions, but now to get foldl, we tell foldMap to work in the Dual monoid.

    foldl :: (b -> a -> b) -> b -> [a] -> b
    foldl g = flip (ala' Endo (ala' Dual foldMap) (flip g))
    

    What is mappend for Dual (Endo b)? Modulo wrapping, it's exactly the reverse composition, >>>.

    0 讨论(0)
  • 2021-02-04 10:18

    foldr basically deconstructs a list, in the canonical way: foldr f initial is the same as a function with patterns:(this is basically the definition of foldr)

     ff [] = initial
     ff (x:xs) = f x $ ff xs
    

    i.e. it un-conses the elements one by one and feeds them to f. Well, if all f does is cons them back again, then you get the list you originally had! (Another way to say that: foldr (:) [] ≡ id.

    foldl "deconstructs" the list in inverse order, so if you cons back the elements you get the reverse list. To achieve the same result with foldr, you need to append to the "wrong" end – either as MathematicalOrchid showed, inefficiently with ++, or by using a difference list:

    reverse'' :: [a] -> [a]
    reverse'' l = dl2list $ foldr (\x accDL -> accDL ++. (x:)) empty l
    
    type DList a = [a]->[a]
    (++.) :: DList a -> DList a -> DList a
    (++.) = (.)
    emptyDL :: DList a
    emptyDL = id
    dl2list :: DLList a -> [a]
    dl2list = ($[])
    

    Which can be compactly written as

    reverse''' l = foldr (flip(.) . (:)) id l []
    
    0 讨论(0)
  • 2021-02-04 10:31

    This is what foldl op acc does with a list with, say, 6 elements:

    (((((acc `op` x1) `op` x2) `op` x3) `op` x4) `op` x5 ) `op` x6
    

    while foldr op acc does this:

    x1 `op` (x2 `op` (x3 `op` (x4 `op` (x5 `op` (x6 `op` acc)))))
    

    When you look at this, it becomes clear that if you want foldl to reverse the list, op should be a "stick the right operand to the beginning of the left operand" operator. Which is just (:) with arguments reversed, i.e.

    reverse' = foldl (flip (:)) []
    

    (this is the same as your version but using built-in functions).

    When you want foldr to reverse the list, you need a "stick the left operand to the end of the right operand" operator. I don't know of a built-in function that does that; if you want you can write it as flip (++) . return.

    reverse'' = foldr (flip (++) . return) []
    

    or if you prefer to write it yourself

    reverse'' = foldr (\x acc -> acc ++ [x]) []
    

    This would be slow though.

    0 讨论(0)
  • 2021-02-04 10:35

    For a start, the type signatures don't line up:

    foldl :: (o -> i -> o) -> o -> [i] -> o
    foldr :: (i -> o -> o) -> o -> [i] -> o
    

    So if you swap your argument names:

    reverse' xs = foldr (\ x acc -> x : acc) [] xs
    

    Now it compiles. It won't work, but it compiles now.

    The thing is, foldl, works from left to right (i.e., backwards), whereas foldr works right to left (i.e., forwards). And that's kind of why foldl lets you reverse a list; it hands you stuff in reverse order.

    Having said all that, you can do

    reverse' xs = foldr (\ x acc -> acc ++ [x]) [] xs
    

    It'll be really slow, however. (Quadratic complexity rather than linear complexity.)

    0 讨论(0)
提交回复
热议问题