Create an Arbitrary instance for a case class that holds a `Numeric` in ScalaCheck?

932 Views Asked by At

I'm specifically trying to define Semigroup and a Sum type which 'is a' Semigroup and check the Associative property of Semigroup generically using ScalaCheck.

I first wrote this out in Haskell because I find it easier to think of these things first in Haskell syntax and then translate them to Scala.

So in Haskell, I wrote the following which works in GHCi:

newtype Sum a = Sum a deriving (Show, Eq)

instance Num a => Num (Sum a) where
  (+) (Sum x) (Sum y) = Sum (x + y)

class Semigroup a where
  (<>) :: a -> a -> a

instance Num a => Semigroup (Sum a) where 
  (<>) = (+)

instance Arbitrary a => Arbitrary (Sum a) where
  arbitrary = fmap Sum arbitrary

semigroupAssocProp x y z = (x <> (y <> z)) == ((x <> y) <> z)
quickCheck (semigroupAssocProp :: Num a => Sum a -> Sum a -> Sum a -> Bool)

I'm trying to create something roughly equivalent in Scala. So far, I have what you see below:

trait Semigroup[A] {
  def |+|(b: A): A
}

case class Sum[A: Numeric](n: A) extends Semigroup[Sum[A]] {
  def |+|(x: Sum[A]): Sum[A] = Sum[A](implicitly[Numeric[A]].plus(n, x.n)
}

val semigroupAssocProp = Prop.forAll { (x: Sum[Int], y: Sum[Int], z: Sum[Int]) =>
  (x |+| (y |+| z)) == ((x |+| y) |+| z)
} 

val chooseSum = for { n <- Gen.chooseNum(-10000, 10000) } yield Sum(n)
// => val chooseSum Gen[Sum[Int]] = org.scalacheck.Gen$$anon$<some hash>

I'm lost on how to create an Arbitrary instance for a more generic Sum[Numeric], or at least a Gen[Sum[Numeric]] and how to create a more generic semigroupAssocProp that could take an x, y, and z of type S where S extends Semigroup[T], with T being any concrete type.

I'm really trying to get as close in functionality to the Haskell version I wrote as possible in Scala.

1

There are 1 best solutions below

10
On BEST ANSWER

Part of the issue is that this is a more direct translation of your Haskell code:

trait Semigroup[A] {
  def add(a: A, b: A): A
}

case class Sum[A](n: A)

object Sum {
  implicit def sumSemigroup[A: Numeric]: Semigroup[Sum[A]] =
    new Semigroup[Sum[A]] {
      def add(a: Sum[A], b: Sum[A]): Sum[A] =
        Sum(implicitly[Numeric[A]].plus(a.n, b.n))
    }
}

It's not a literal translation, since we don't supply a Numeric instance for Sum[A] (which would be more of a pain, given Numeric's interface), but it does represent the standard encoding of type classes in Scala.

Now you provide an Arbitrary instance for Sum[A] in exactly the same way as in Haskell:

import org.scalacheck.Arbitrary

implicit def arbitrarySum[A](implicit A: Arbitrary[A]): Arbitrary[Sum[A]] =
  Arbitrary(A.arbitrary.map(Sum(_)))

And then you can define your property:

import org.scalacheck.Prop

def semigroupAssocProp[A: Arbitrary: Semigroup]: Prop =
  Prop.forAll { (x: A, y: A, z: A) =>
    val semigroup = implicitly[Semigroup[A]]

    semigroup.add(x, semigroup.add(y, z)) == semigroup.add(semigroup.add(x, y), z)
  }

And then check it:

scala> semigroupAssocProp[Sum[Int]].check
+ OK, passed 100 tests.

The key point is that Scala doesn't encode type classes using subtyping in the way that your implementation tries to do—instead you define your type classes as traits (or classes) that look very similar to the way you use class in Haskell. My Semigroup's |+|, for example, takes two arguments, just like the <> in the Haskell Semigroup. Instead of a separate instance-like language-level mechanism, though, you define your type class instances by instantiating these traits (or classes) and putting the instances into implicit scope.