zipWith

[a]

[b]

a->b->c

[c]

zipWith3

zipWith4

zipWith7

map

zipWith1

repeat

zipWith0

Data.Sequence

zipWith

We describe an alternative: the single function to elementwise combine arbitrarily many collections of arbitrary sorts (lists, sequences, etc). We hence present the double-generic zipWith -- generic in the number of arguments and in the sort of the collections. Different collections may have elements of different types; in some implementations, different collections may be of different sorts -- some arrays, some lists, some unpacked arrays or sequences. We do not have to separately specify the count of the collections to zip. Before we go any further, here are a few examples. First, we sum up two or three lists:

test1 :: [Int] test1 = zipN succ [1,2,3] test2 :: [Int] test2 = zipN (+) [1,2,3] [10,11,12] test21 :: [Int] test21 = zipN (((+) .) . (+)) [1,2,3] [10,11,12] [100,200,300]

mask

arr1

arr2

Float

(-1,7)

(2,20)

Float

(2,7)

a3 = zipN (+) arr1 arr2 :: Array Int Float ar = zipN (\x y z -> if x then y else z) mask arr1 a3 :: Array Int Float

zipN

Integer

ByteString

[Char]

ByteString

bstr :: B.ByteString test51 = zipN B.pack (0x5::Integer) bstr "xyz" (With $ \x y z -> if x then y else (fromIntegral . ord $ z))

We present three implementations of zipWith -- in the classical triad of thesis, antithesis and synthesis. All three are usable, differing in the sorts of collections they support and if all collections to zip must be of the same sort. They also greatly differ in efficiency: the last, synthesis implementation is most efficient, creating no intermediate collections, filling in the result as it steps through the argument collections in parallel.

We start with the most obvious, and the least efficient implementation. Recall, our goal is to generalize the family

map :: (a -> b) -> [a] -> [b] zipWith :: (a -> b -> c) -> [a] -> [b] -> [c] zipWith3 :: (a -> b -> c -> d) -> [a] -> [b] -> [c] -> [d] ...

zipN :: (a1 -> a2 -> a3 -> ... -> w) -> c a1 -> c a2 -> ... -> c w

c

class Zip2 c where zipW2 :: (a->b->r) -> c a -> c b -> c r

The idea of the first implementation comes from the similarity between zipping and the Applicative Functor application: zipN f a1 a2 ... an is essentially f <$> a1 <*> a2 ... <*> an , assuming the collection type implements the Applicative interface, or at least the (<*>) operation. Anything that implements the Zip2 interface is such an unpointed Applicative: (<*>) = zipW2 ($) . The problem hence reduces to writing a function that takes an arbitrary number of arguments and ``inserts (<*>) between them''.

The Introduction section of the page has already described the solution: the type class that pattern-matches on the return type, the type of the continuation. Here it is, adjusted to our circumstances:

class ZipN ca r where zip_n :: ca -> r -- The return type is (c r): produce the result instance (a ~ r) => ZipN (c a) (c r) where zip_n = id -- The return type is (c a' -> r): we are given a new -- collection. We combine it with the accumulator using <*> instance (Zip2 c, a ~ a', ZipN (c b) r) => ZipN (c (a -> b)) (c a' -> r) where zip_n cf ca = zip_n (zipW2 ($) cf ca) -- Our desired zipN is hence zipN f c1 = zip_n (fmap f c1)

This was the entire implementation. The just defined zipN can be used as:

mask :: Array Int Bool a1, a2 :: Array Int Float a3 = zipN (+) a1 a2 :: Array Int Float -- element-wise addition ar = zipN (\x y z -> if x then y else z) mask a1 a3 :: Array Int Float -- selection according to the mask

In this very simple implementation of zipN , all collections to zip must be of the same sort (e.g., all lists) and implement Zip2 and Functor . Signatures are mandatory, to tell zipN there are no more arguments. This is a drawback. The implementation breaks down for collections that are functions: the instances of ZipN become overlapping. A more serious problem is allocating large amounts of working memory. For example, the zipping of three collections zipN (f::t1->t2->t3->r) (a1::c t1) (a2::c t2) (a3::c t3) proceeds as:

let w1 = fmap f a1 :: c (t2->t3->r) w2 = w1 <*> a2 :: c (t3 -> r) w3 = w2 <*> a3 :: c r in w3

w2

w3

As the antithesis, we negate some design decisions and improve the type inference, avoiding the cumbersome type annotations. We make the combining function the last argument of zipN , and wrap it in the unique newtype With . The type of zipN now has the following general shape

zipN :: c a1 -> c a2 -> .... c an -> (With (a1 -> a2 -> ... an -> r)) -> c r

With

zipN (a1::c t1) (a2::c t2) (a3::c t3) (With (f::t1->t2->t3->r))

let w1 = a1 :: c t1 cnv1 = id :: forall u. (t1->u) -> (t1->u) w2 = zipW2 (,) w1 a2 :: c (t1,t2) cnv2 = \g -> \ (a1,a2) -> cnv1 g a1 a2 :: forall u. ((t1 -> t2 -> u) -> ((t1,t2) -> u) w3 = zipW2 (,) w2 a3 :: c ((t1,t2),t3) cnv3 = \g -> \ (a12,a3) -> cnv2 g a12 a3 :: forall u. ((t1 -> t2 -> t3 -> u) -> (((t1,t2),t3) -> u) in fmap (cnv3 f) w3

a3 = zipN arr1 arr2 (With (+)) ar = zipN mask arr1 a3 (With (\x y z -> if x then y else z))

The annoying type signatures are gone: all the types are inferred. The argument collections still have to be of the same sort and be zippable -- but they can now be functions. We are building collections of tuples rather than of closures, which is marginally better: tuples take less space. Nevertheless, we are still building the intermediate collections w2 and w3 , wasting time and space. There has to be a better way.

As synthesis, we look back to the starting point, the standard library function zipWith . Thanks to lazy lists, it traverses its two argument lists in parallel: it takes an element from each list, combines them into the result element, and only then looks further through the argument lists. Thus zipping up multiple lazy lists avoids building intermediate lists thanks to laziness. To handle other collections just as efficiently, we have to lazily convert them to lazy lists. In other words, the proper zipping should deal not with the collections themselves but with their views, as lazy element streams. Following this insight, we define the interface for a streaming view of a collection:

class Streamable c where type StreamEl c :: * toStream :: c -> [StreamEl c]

Foldable

Streamable

ByteString

Streamable

Streamable

zipN

zipN (tr :: [r] -> c r) (a1::c1 t1) (a2::c2 t2) (a3:: c3 t3) (With (f::t1->t2->t3->r))

let w1 = toStream a1 :: [t1] cnv1 = id :: forall u. (t1->u) -> (t1->u) w2 = zip w1 (toStream a2) :: [(t1,t2)] cnv2 = \g -> \ (a1,a2) -> cnv1 g a1 a2 :: forall u. ((t1 -> t2 -> u) -> ((t1,t2) -> u) w3 = zip w2 (toStream a3) :: [((t1,t2),t3)] cnv3 = \g -> \ (a12,a3) -> cnv2 g a12 a3 :: forall u. ((t1 -> t2 -> t3 -> u) -> (((t1,t2),t3) -> u) in tr $ map (cnv3 f) w3

zipN

We have described the double-generic zipN that efficiently combines arbitrarily many collections of arbitrary types. The key ideas are pattern-matching on the result type (the type of the continuation); defining the interface of zipN to make this pattern-matching unambiguous; operating on the streaming view of collections rather than the collections themselves.