`Applicative[OneAnd[ZipLazyList, *]]` works incorrectly
Nobody has claimed this yet.
- Dominant language
- Scala
- Stars
- 5.5k
- Forks
- 1.2k
- Avg merge
- 2d 10h
- Merged PRs (30d)
- 5
Description
A follow-up to #4830.
Consider the snippet:
//> using scala 3.8.2
//> using options -Xkind-projector
//> using dep org.typelevel::cats-core:2.13.0
import cats.Applicative
import cats.data.OneAnd
import cats.data.ZipLazyList
val s1 = LazyList(1, 2, 3)
val s2 = LazyList(4, 5)
val sR = Applicative[LazyList].product(s1, s2)
println("- LasyList: " + sR.mkString(","))
val ns1 = OneAnd(s1.head, s1.tail)
val ns2 = OneAnd(s2.head, s2.tail)
val nsR = Applicative[OneAnd[LazyList, *]].product(ns1, ns2)
println("- NeLasyList: " + (nsR.head #:: nsR.tail).mkString(","))
val z1 = ZipLazyList(s1)
val z2 = ZipLazyList(s2)
val zR = Applicative[ZipLazyList].product(z1, z2)
println("- ZipLasyList: " + zR.value.mkString(","))
val nz1 = OneAnd(s1.head, ZipLazyList(s1.tail))
val nz2 = OneAnd(s2.head, ZipLazyList(s2.tail))
val nzR = Applicative[OneAnd[ZipLazyList, *]].product(nz1, nz2)
println("- NeZipLasyList: " + (nzR.head #:: nzR.tail.value).mkString(","))
It prints the following:
- LasyList: (1,4),(1,5),(2,4),(2,5),(3,4),(3,5)
- NeLasyList: (1,4),(1,5),(2,4),(2,5),(3,4),(3,5)
- ZipLasyList: (1,4),(2,5)
- NeZipLasyList: (1,4),(1,5),(2,4),(3,4)
Note: it works the same way on Scala 2.13 and for Stream type on Scala 2.12
As you can see, while Applicative[F].product produces identical results for both LazyList and OneAnd[LazyList, *],
for ZipLazyList and OneAnd[ZipLazyList, *] it doesn't. Moreover, the latter is neither the Cartesian product nor element-wise composition – it's something different.
I would argue that OneAnd for a collection type should behave as the non-empty variant of that collection. That is not the case for ZipLazyList, apparently.
The reason appears to be the Alternative[ZipLazyList] instance discussed in #4830 – it seems to be unlawful. The problem for OneAnd is that its Applicative instance requires an Alternative for the underlying type F:
https://github.com/typelevel/cats/blob/8baa9bd2ff2185ea8faf88849ecee1b2da337464/core/src/main/scala/cats/data/OneAnd.scala#L248
There's something off with OneAnd:
- if we assume that
Alternative[ZipLazyList]is incorrect indeed, then the currentApplicativeimplementation forOneAndimplies that it cannot work correctly forZip*collections. - if we still believe that
Alternative[ZipLazyList]is good enough forOneAnd, thenApplicativeforOneAndis not correctly implemented. - or, perhaps, we cannot treat
OneAndas just a non-empty "drop-off" for the underlyingF.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Reproduce the supplied Scala snippet with cats-core 2.13.0 and compare the LazyList, OneAnd[LazyList, *], ZipLazyList, and OneAnd[ZipLazyList, *] results. Read the OneAnd Applicative implementation in core/src/main/scala/cats/data/OneAnd.scala around line 248 and the Alternative discussion in #4830. Done requires a decided and documented correct relationship between these instances.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- scala
- Domain
- backend-api-design
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 30/100