13

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).

6
  • 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. Mar 2, 2015 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? Mar 2, 2015 at 17:30
  • @KilianFoth, get an exception thrown. Mar 2, 2015 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). Mar 2, 2015 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.
    – Weaver
    Mar 4, 2015 at 6:33

2 Answers 2

13

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.

4
  • 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. Mar 2, 2015 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. Mar 2, 2015 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. Mar 3, 2015 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. Mar 4, 2015 at 3:40
11

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.

5
  • 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? Mar 2, 2015 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, 2015 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, 2015 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 Mar 2, 2015 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, 2015 at 21:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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