12

C#, Scala, Haskell, Lisp and Python have the same zip behaviour: if one collection is longer, the tail is silently ignored.

It could be an exception thrown as well, but I didn't heard of any language using this approach.

This puzzles me. Does anyone know the reason why zip is designed that way? I guess for new languages, it is done because other languages do it this way. But what was the root reason?

I am asking here factual, historic-based question, not if somebody likes it, or if it is a good or bad approach.

Update: If I was asked what to do, I would say -- throw an exception, pretty similarly to indexing an array (despite "old" languages did all kind of magic, how to handle out of bounds index, UB, expand array, etc).

  • 10
    If it didn't ignore the tail one functor has, using infinite sequences would be more cumbersome. Especially if getting the length of the non-infinite range was expensive/convoluted/impossible. – Deduplicator Mar 2 '15 at 17:29
  • 2
    You appear to think that this is unexpected and strange. I find it obvious and, indeed, inevitable. What would you want to happen when you zip collections of unequal length? – Kilian Foth Mar 2 '15 at 17:30
  • @KilianFoth, get an exception thrown. – greenoldman Mar 2 '15 at 17:31
  • @Deduplicator, nice one. With silent tail drop you can naturally express zipWithIndex providing natural numbers generator. Now, the only missing piece of info -- what was it the reason? :-) (btw. please repost your comment as an answer, thank you). – greenoldman Mar 2 '15 at 17:39
  • 1
    Python has itertools.izip_longest, which effectivly autopads finished inputs with Nones. I choose it over zip frequently when i actually use zip; i can't remember the reasons behind any choice anymore though. Python has enumerate() for @greenoldman's case already, which i do use often. – StarWeaver Mar 4 '15 at 6:33
11

It's almost always what you want, and when it isn't, you can do the fill yourself.

The main issue is with lazy semantics you don't know the length when you first start the zip, so you can't just throw an exception at the start. You would need to first return all the common elements, then throw an exception, which wouldn't be very useful.

It's also a style issue. Imperative programmers are accustomed to manually checking boundary conditions all over the place. Functional programmers prefer constructs that can't fail by design. Exceptions are extremely rare. If there's a way for a function to return a reasonable default, functional programmers will take it. Composability is king.

  • I am asking about historic reasons, not what I can do. Second paragraph -- you are wrong, take a look how zip is currently implemented. Throwing exception is simply changing "stop yield" to "throw". Third paragraph -- returning empty element for reaching out of boundary cannot fail, but yet I doubt any FP dev would vote it is a good design. – greenoldman Mar 2 '15 at 22:15
  • 3
    My second paragraph doesn't apply to all the implementations, only the truly lazy ones. If you zip two infinite sequences together, you don't know the size at the start. On the third paragraph, I said reasonable default. Returning empty in this case wouldn't be reasonable, whereas dropping the tail obviously is. – Karl Bielefeldt Mar 2 '15 at 22:31
  • Ah, I see your point finally -- with throwing exception in lazy language it is not technical replacement, it is completely change of behaviour, because you need throw an exception right at the start, while you can ignore tail whenever it is convenient. – greenoldman Mar 3 '15 at 7:16
  • 3
    +1 this is also a great answer, "Functional programmers prefer constructs that can't fail by design" this so eloquently states what is the largest motivator behind a majority of design decisions functional programmers make. Imperative programmers have a rule they like that says "Tell, don't ask", FP takes this to the Nth degree by focusing on allowing continuous telling of instructions without requiring results checking until the absolute last moment, so we try to ensure intermediate steps can't fail, because Composability is king. Very well said. – Jimmy Hoffa Mar 4 '15 at 3:40
12

Because there's no obvious way to complete the tail. Any choice on how to do it would result in a non-obvious tail.

The trick is to explicitly lengthen your shortest list to match the length of the longest with values you expect.

If zip did that for you, you couldn't know what values it was filling in intuitively. Did it cycle the list? Did it repeat a mempty value? What is a mempty value for your type?

There's no implication in what zip does which one could use to intuit the way the tail would be lengthened, so the only reasonable thing to do is work with the values available rather than making some up your consumer may not expect.


Also remember you're referring to a very specific well known function with specific well known semantics. But that doesn't mean you can't make a similar but slightly different function. Just because there's a common function that does x, doesn't mean you can't decide for your given purpose you want to do x and y.

Though remember the reason this and many other common FP style functions are common, is because they're simple and generalized so you can tweak your code to use them and get the behaviour you want. For instance, in C# you could just

IEnumerable<Tuple<T, U>> ZipDefaults(IEnumerable<T> first, IEnumerable<U> second)
{
    return first.Count() < second.Count()
        ? first.Concat(Enumerable.Repeat(default(T), second.Count() - first.Count())).Zip(second)
        : first.Zip(second.Concat(Enumerable.Repeat(default(U), first.Count() - second.count())))
}

Or other simple things. FP approaches make modifications so easy because you can reuse pieces as well as having implementations be so small as above that creating your own modified versions of things is exceedingly simple.

  • Ok, but it is only when you force the collections to do something to match other -- compare it to indexing the collection (array). You could start thinking should I expand and array if I have index out of bounds? Or maybe silently ignore the request. But for some time there is common notion of throwing exception. Same here -- if you don't have matching collection, throw an exception. Why this approach was not taken? – greenoldman Mar 2 '15 at 17:35
  • 2
    zip could fill in nulls, which is often an intuitive solution. Consider the type zip :: [a] -> [b] -> [(Maybe a, Maybe b)]. Granted, the result type is a bit^H^H quite impractical, but it would allow to easily implement any other behaviour (short cut, exception) on top of it. – amon Mar 2 '15 at 17:37
  • 1
    @amon: That is not intuitive at all, it's silly. It would just require null checking every argument. – DeadMG Mar 2 '15 at 19:29
  • 4
    @amon not every type has a null, that's what I meant by mempty, objects have null to fill the space, but you want it to have to come up with such a thing for int and other types as well? Sure, C# has default(T) but not all languages do, and even for C# is that really obvious behaviour? I don't think so – Jimmy Hoffa Mar 2 '15 at 19:51
  • 1
    @amon It'd probably be more useful to return the unconsumed part of the longer list. You can use that to check if they were equal length after-the-fact if you need to, and can still re-zip or do something with the unconsumed tail without re-traversing the list. – Doval Mar 2 '15 at 21:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.