typelevel / scalacheck

Property-based testing for Scala
http://www.scalacheck.org
BSD 3-Clause "New" or "Revised" License
1.94k stars 407 forks source link

Introduce `Gen.zipWith` multi-arity method #1062

Open satorg opened 4 months ago

satorg commented 4 months ago

Adds a code generator for Gen.zipWith functions.

Example:

case class Foo(pos: Int, ascii: String, hex: String)

Gen.zipWith(Gen.posNum[Int], Gen.asciiStr, Gen.hexStr)(Foo.apply)

Without Gen.zipWith the same was possible to accomplish with Gen.zip, however it would required an additional map over an intermediary tuple instance:

Gen.zip(Gen.posNum[Int], Gen.asciiStr, Gen.hexStr)
  .map { case (pos, ascii, hex) => Foo(pos, ascii, hex)) }

Therefore Gen.zipWith allows to avoid the intermediate conversion which can come in handy in come cases.

Note that Gen.zipWith starts with arity 2 whereas Gen.zip starts with arity 1. However, I think the latter was an oversight – Gen.zip for arity 1 is a no-op function.

rossabaker commented 3 months ago

Is this intended to behave similarly to mapN, but without the Cats dependency?

satorg commented 3 months ago

Is this intended to behave similarly to mapN, but without the Cats dependency?

Pretty much. To be more accurate, it is supposed to resemble functions map2 through map22 of the Apply typeclass, because mapN requires a tuple to be created first, check this out:

case class Foo(pos: Int, ascii: String, hex: String)

val gen1 = (Gen.posNum[Int], Gen.asciiStr, Gen.hexStr).mapN { Foo.apply }

val gen2 = Apply[Gen].map3(Gen.posNum[Int], Gen.asciiStr, Gen.hexStr) { Foo.apply }

val gen3 = Gen.zipWith(Gen.posNum[Int], Gen.asciiStr, Gen.hexStr) { Foo.apply }

However, there's one catch with the Cats functions: they both use a chain of Functor.product under the hood, which in turn calls to Gen.zip with arity 2. I.e. the Cats functions incur a bunch of intermediary tuples on every evaluation to be created.