Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement zip with foldl (in an eager language)

A Clojure programmer I know recently remarked that it's possible to implement lots of sequence functions in terms of Clojure's reduce (which is Haskell's foldl'), but that regrettably there's no way to implement (map list xs ys) (which is Haskell's zip) with just reduce.

Now, I've read about the universality of folds, so I'm pretty sure this isn't true: certainly zip is possible with foldr, and I'd be surprised if it weren't possible with foldl. For purposes of this discussion we're ignoring the problems of the eagerness of foldl compared to foldr: imagine we have unlimited memory and only work with finite sequences.

So, I took the Haskell code to implement zip with foldr, and translated it to Clojure, doing my best to adjust for the difference between foldr and foldl:

(defn zip [xs ys]
  (letfn [(done [_] [])
          (step [z x]
            (fn [ys]
              (if (empty? ys)
                []
                (conj (z (rest ys)) [x (first ys)]))))]
    ((reduce step done xs) ys)))
user> (zip [1 2] '[a b c]) ;=> [[1 b] [2 a]]

And indeed we do get pairs of elements from xs and ys tupled together, but out of order: the first element of xs is paired with the last element of ys, and so on down the line. I can kinda see the cause of the problem: the function we produce consumes ys starting from the left, but the outermost closure (which is called first) has closed over the last element of xs, so it can't pair them in the right order.

I think the fix is to somehow build the closures inside-out, but I can't really see how to do it. I would happily accept a solution in either Haskell or Clojure.

I'm hoping for a solution of the form zip = foldl f x, so that I can say it's "just" a reduce. Of course I can reverse one of the lists, but then it ends up looking like zip xs ys = foldl f x xs $ reverse ys which doesn't seem very satisfying or clean.

like image 596
amalloy Avatar asked Jan 24 '15 22:01

amalloy


People also ask

Is Foldl or Foldr better?

Only foldr is lazy and can be used for codata/infinite streams. While foldl is tail-recursive (enhanced with strict application foldl' can avoid stack overflow). This is why foldr should be used by default in Haskellin order preserve laziness across function composition.

What is the difference between Foldl and Foldl?

Difference Between foldl and foldr The difference is that foldl is tail-recursive, whereas foldr is not. With foldr and non-optimized patterns, proc is applied to the current value and the result of recursing on the rest of the list. That is, evaluation cannot complete until the entire list has been traversed.

How does foldl work?

With foldl , the function argument takes a default value, the first element of the list, and returns a new default value. When the list is empty, that default value will be the result of the fold. This is admittedly a simple example.


1 Answers

In Haskell:

-- foldr f z xs == foldl (\r x a-> r (f x a)) id xs z

zip1 xs ys = -- foldr f (const []) xs ys
            foldl (\r x a-> r (f x a)) id xs (const []) ys
  where
    f x r []     = []
    f x r (y:ys) = (x,y) : r ys

Prelude> zip1 [1..9] [100..120]
[(1,100),(2,101),(3,102),(4,103),(5,104),(6,105),(7,106),(8,107),(9,108)]

Since Clojure likes appending at the end of list, another variant is

zip2 xs ys = foldl f (const []) xs ys
  where
    f r x [] = []
    f r x ys = let (ys0,y) = (init ys, last ys) -- use some efficient Clojure here
               in r ys0 ++ [(x,y)]

Prelude> zip2 [1..9] [100..120]
[(1,112),(2,113),(3,114),(4,115),(5,116),(6,117),(7,118),(8,119),(9,120)]

As you can see the end of lists lines up here, instead of the front.

like image 100
Will Ness Avatar answered Sep 28 '22 02:09

Will Ness