question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

`Seq.zip`, `Seq.map2` etc behave different from `List.zip`, `List.map2`, `Array.zip` etc w.r.t. raising for different lengths

See original GitHub issue

In cases where you apply a pairwise operation to two sequences, like Seq.zip, the behavior in F# Core is defined by the implementation of Seq.map2 and List.map2 and the like. The behavior between collection types is different, however.

  • Array.zip and List.zip throw an ArgumentException whenever the sequences have differing lengths
  • Seq.zip on the other hand doesn’t.

I doubt this behavior can be changed (backwards compatibility), but I do think it is a bug/oversight or whatchamacallit. I’m currently working on implementing and extending TaskSeq, based on @dsyme’s original code from this repo and raised this as a question to myself: https://github.com/abelbraaksma/TaskSeq/issues/32. Then I figured, let’s broaden the discussion scope 😉.

Repro steps

// this is fine
[1;2;3] |> Seq.zip ["one"; "two" ] |> Seq.toList

// this raises
[1;2;3] |> List.zip ["one"; "two" ]

// this raises too
[1;2;3] |> Array.zip ["one"; "two" ]

Also, this is quite weird:

// returns true??
[1;2;3] |> Seq.forall2 (=) Seq.empty  // true
[1;2;3] |> Seq.forall2 (=) [1;2;3;4]  // true

[1;2;3] |> List.forall2 (=) Seq.empty  // exception
[1;2;3] |> List.forall2 (=) [1;2;3;4]  // exception

Expected behavior

The same behavior for all collection types.

Actual behavior

Functions like Seq.map2, Seq.map3, Seq.mapi2, Seq.zip do not raise an ArgumentException when the sizes are different. However, the implementations do read past the end of the sequence and even if false, read the next item of the paired sequence as well (see MapEnumerator code here). In other words, the information whether one or both sequences are exhausted is available.

Known workarounds

In lazily evaluated sequences, the only workaround is to “roll your own”. Easy enough, but still. Alternatively, you could, of course, cache the sequences as an eager sequence like List or Array.

Related information

I did try to find a motivation for this behavior in the source code an online, but failed to do so. There’s certainly an argument to be made for not raising an exception, but then one would expect that to be the case for all collection types.

Perhaps there’s something with lazy sequences that suggest not raising exceptions in general. But something like [1..3] |> Seq.take 4 raises (not immediately, but when iterating over the sequence), in other words, it does not seem to be a taboo.

Issue Analytics

  • State:closed
  • Created a year ago
  • Comments:11 (10 by maintainers)

github_iconTop GitHub Comments

1reaction
ly29commented, Oct 17, 2022

Altering this behavior would be a breaking change. I like the python like behaivour of not throwing things which make List.zip unusable in many cases. Compare to the slice operations that are safe now.

0reactions
abelbraaksmacommented, Oct 17, 2022

I understand the argument “it’s always been this way”, and that we can’t change it. I don’t understand the rationale, as none of my examples require iteration of the whole sequence to throw. In fact, already during the standard operation, all information is available to throw or not (mainly, the two or three booleans that know whether to continue).

This is different from Haskell, btw, in which the order of arguments determines which of the MoveNexts are called, which IMO is wrong in a different way (non commutative arguments).

Anyway, fair enough to close this out, I agree we shouldn’t change the defaults here.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Lists and Arrays should stop throwing on different lengths ...
Lists and Arrays should stop throwing on different lengths for map2, map3, zip, zip3 etc. I propose we remove this inconsistency: let x...
Read more >
Zip two lists of different lengths with default element to fill
Now it works with this solution: list1.zipAll(list2, "", "") map ({ case (y, x) => y + x }) but not like before...
Read more >
Does the zip function work with lists of different lengths?
Answer. The zip() function will only iterate over the smallest list passed. If given lists of different lengths, the resulting combination will ...
Read more >
How to merge two sequential collections into pairs with 'zip'
Solution. Use the zip method that's available to Scala sequential collections to join two sequences into one: scala> val women = List(" ...
Read more >
XPath and XQuery Functions and Operators 3.1
Operators such as "+" may be overloaded: they map to different underlying functions depending on the dynamic types of the supplied operands.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found