typelevel / typelevel/scalacheck

Overloaded Prop.collect behaves surprisingly with Seq[T] argument

Open
#421 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Scala
Stars
2k
Forks
393
Avg merge
6h 42m
Merged PRs (30d)
4

Description

Summary

Prop.collect is overloaded. There is a "simple" version

  • collect(T)(Prop): Prop

and a "functional" version

  • collect(T=>P)(implicit P=>Prop): T=>Prop

Using collect in properties based on generators that produce Seq[T] values (for example Gen.pick) can lead to potentially surprising behavior. In the example detailed below, the compiler selects the functional version over the simple version when the argument is of type Seq[T] with a Type mismatch compiler error.

This function version selection occurs because Seq[T] extends PartialFunction[Int, T] which matches (in part) the signature collect(Int => T)(implicit T => Prop).

Once the issue is understood, there are straightforward workarounds.

Improved documentation in the source file for the two collect methods seems a straightforward way to mitigate potentially complex and frustrating diagnosis by your typical developer (assuming she gets as far a clicking through to the Scalacheck sources, or pulling up the equivalent of Intellij's quick documentation).

Removing the overload completely by renaming the functional version is a more ambitious solution.

Details

I was using collect in a property along these lines

val prop = forAll(Gen.pick(2, (0 to 9)) { ns => ns.size == 2 }

which I then augmented with collect

val prop = forAll(Gen.pick(2, (0 to 9)) { ns => collect(ns) { ns.size == 2 }}

At which point I get this mysterious (to me, at the time) compiler error

Type mismatch: expected: Int => Prop, actual: Boolean

Eventually I figured out what was going on, but it took a while, mostly because my eyes start to glaze over when I end up looking at a declaration like:

def collect[T, P](f: T => P)(implicit ev: P => Prop): T => Prop = ...

The problem is that pick is type Gen[Seq[T]], and Seq[T] extends PartialFunction[Int,T]. This explains the compiler's decision to use collect(Int=>T) instead of collect(Seq[T]). (At least in part, I wonder why it doesn't complain about ambiguous overloading. But that is a topic for another day.)

Workarounds are relatively simple, such as

  • Cast to Iterable (which is not a function type)
    val prop = forAll(Gen.pick(2, (0 to 9)) { ns => collect(ns.toIterable) { ns.size == 2 }}

  • Convert to String
    val prop = forAll(Gen.pick(2, (0 to 9)) { ns => collect(ns.toString) { ns.size == 2 }}

  • Rewrite to intentionally use functional collect(Seq[Int] => Boolean)
    val prop = forAll(gen)(collect{ns: Seq[Int] => ns.size == 2})

So the issue is ergonomic, not functional. Surprising and unexpected behavior like this can be frustrating and a time waster.

In my limited experience as a Scalacheck user, my preference is for the collect(T) form over the collect(T=>P) form, in part because the former allows more manipulation of the collected values.

Suggestions

  • Eliminate the overload by renaming collect(T=>P)(implicit P=>Prop)
  • Provide more guidance in the source documentation. Current doc is skimpy:
    /** Collect data for presentation in test report */
    Documentation could be enhanced to note the potentially surprising behavior when collect's
    argument is a Seq. This would have saved me quite a bit of time, since Intellij certainly took me
    right to that line in Prop.scala.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in the Prop.scala source file and inspect the documentation for both collect overloads. Clarify how a Seq argument can select the functional overload, and document the available workarounds described in the issue. Done means the source documentation makes this overload interaction and the resulting compiler error understandable.

Written by the indexing model from the issue text.

Assessment

Tech stack
scala
Domain
testing
Issue type
Documentation
Difficulty
1/5
Estimated time
Under an hour
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.