Skip to content

Applicative[OneAnd[ZipLazyList, *]] works incorrectly #4840

@satorg

Description

@satorg

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:

implicit def catsDataApplicativeForOneAnd[F[_]](implicit F: Alternative[F]): Applicative[OneAnd[F, *]] =

There's something off with OneAnd:

  • if we assume that Alternative[ZipLazyList] is incorrect indeed, then the current Applicative implementation for OneAnd implies that it cannot work correctly for Zip* collections.
  • if we still believe that Alternative[ZipLazyList] is good enough for OneAnd, then Applicative for OneAnd is not correctly implemented.
  • or, perhaps, we cannot treat OneAnd as just a non-empty "drop-off" for the underlying F.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions