Skip to content

Commit

Permalink
Scala 2.13.0 and Hash derivation rework
Browse files Browse the repository at this point in the history
Case classes' `hashCode` is not backwards compatible in Scala 2.13,
because it mixes the `productPrefix` for better distribution.
This means we need version specific code to pass the `Hash` laws.

  * Update dependencies for Scala 2.13.0
  * Rework `Hash` derivation for consistency
  * Split product and coproduct derivation
  * Add a Scala version specific seed for `scala.Product` subtypes
  * Optimize `HashBuilder` to a one-pass hash
  * Replace usages of `Statics` (not recommended) with `MurmurHash3`
  * Test all variants of `Hash` derivation
  • Loading branch information
joroKr21 committed Jun 13, 2019
1 parent 41f0abd commit 65c637b
Show file tree
Hide file tree
Showing 12 changed files with 151 additions and 168 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ language: scala
scala:
- 2.12.8
- 2.11.12
- 2.13.0-M5
- 2.13.0-RC3

jdk:
- oraclejdk8
Expand Down
15 changes: 6 additions & 9 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import sbtcrossproject.{CrossType, crossProject}

lazy val buildSettings = Seq(
organization := "org.typelevel",
scalaVersion := "2.12.8",
crossScalaVersions := Seq("2.11.12", scalaVersion.value, "2.13.0-M5")
scalaVersion := "2.13.0",
crossScalaVersions := Seq("2.11.12", scalaVersion.value, "2.13.0")
)

val catsVersion = "1.6.0"
val catsVersion = "2.0.0-M4"

lazy val commonSettings = Seq(
scalacOptions := Seq(
Expand All @@ -22,11 +22,8 @@ lazy val commonSettings = Seq(
),
scalacOptions ++= (
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, v)) if v <= 12 => Seq(
"-Ypartial-unification"
)
case _ => Seq(
)
case Some((2, v)) if v <= 12 => Seq("-Ypartial-unification")
case _ => Seq.empty
}
),
resolvers ++= Seq(
Expand All @@ -39,7 +36,7 @@ lazy val commonSettings = Seq(
"org.typelevel" %% "alleycats-core" % catsVersion,
"com.chuusai" %% "shapeless" % "2.3.3",
"org.typelevel" %% "cats-testkit" % catsVersion % "test",
compilerPlugin("org.spire-math" %% "kind-projector" % "0.9.9")
compilerPlugin("org.typelevel" %% "kind-projector" % "0.10.3")
),
scmInfo :=
Some(ScmInfo(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package cats.derived.util

import scala.util.hashing.MurmurHash3

private[derived] object VersionSpecific {
def productSeed(x: Product): Int = MurmurHash3.productSeed
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package cats.derived.util

import scala.util.hashing.MurmurHash3

private[derived] object VersionSpecific {
def productSeed(x: Product): Int = MurmurHash3.productSeed
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package cats.derived.util

import scala.util.hashing.MurmurHash3

private[derived] object VersionSpecific {
def productSeed(x: Product): Int = MurmurHash3.mix(MurmurHash3.productSeed, x.productPrefix.hashCode)
}
147 changes: 79 additions & 68 deletions core/src/main/scala/cats/derived/hash.scala
Original file line number Diff line number Diff line change
@@ -1,91 +1,102 @@
package cats.derived
package cats
package derived

import cats.Hash
import shapeless._, labelled._
import scala.annotation.{implicitNotFound, tailrec}
import scala.util.hashing.MurmurHash3

@implicitNotFound("Could not derive an instance of Hash[${A}]")
trait MkHash[A] extends Hash[A]



object MkHash extends MkHash0 {
def apply[A](implicit hash: MkHash[A]): MkHash[A] = hash


object MkHash extends MkHashDerivation {
def apply[A](implicit ev: MkHash[A]): MkHash[A] = ev
}

private[derived] trait HashBuilder[A] {
def hashes(a: A): List[Int]
def hashes(x: A): List[Int]
def eqv(x: A, y: A): Boolean
}

object HashBuilder {
implicit val emptyProductDerivedHash: HashBuilder[HNil] = new HashBuilder[HNil] {
override def hashes(x: HNil): List[Int] = Nil
override def eqv(x: HNil, y: HNil): Boolean = true
}


implicit def productDerivedHash[H, T <: HList](
implicit
hashH: Hash[H] OrElse MkHash[H],
hashT: HashBuilder[T]): HashBuilder[H :: T] = new HashBuilder[H :: T] {
def hashes(fields: H :: T): List[Int] = {
val h = hashH.unify.hash(fields.head)
val t = hashT.hashes(fields.tail)
h :: t
def hash(x: A, seed: Int): Int = {
@tailrec def loop(hashes: List[Int], hash: Int, length: Int): Int = hashes match {
case head :: tail => loop(tail, MurmurHash3.mix(hash, head), length + 1)
case Nil => MurmurHash3.finalizeHash(hash, length)
}

def eqv(x: H :: T, y: H :: T): Boolean =
hashH.unify.eqv(x.head, y.head) && hashT.eqv(x.tail, y.tail)
loop(hashes(x), seed, 0)
}
}

trait MkHash0 extends MkHash1 {
implicit def deriveHashCaseObject[A](
implicit repr: Generic.Aux[A, HNil]): MkHash[A] = new MkHash[A] {
def hash(x: A): Int = x.hashCode
private[derived] object HashBuilder {
import shapeless._

def eqv(x: A, y: A): Boolean = true
}
implicit val hashBuilderHNil: HashBuilder[HNil] =
instance(_ => Nil, (_, _) => true)

}
implicit def hashBuilderHCons[H, T <: HList](
implicit H: Hash[H] OrElse MkHash[H], T: HashBuilder[T]
): HashBuilder[H :: T] = instance(
{ case h :: t => H.unify.hash(h) :: T.hashes(t) },
{ case (hx :: tx, hy :: ty) => H.unify.eqv(hx, hy) && T.eqv(tx, ty) }
)

trait MkHash1 {
implicit def fromBuilder[A](implicit builder: HashBuilder[A]): MkHash[A] = new MkHash[A] {
override def hash(x: A): Int = {
val hashes = builder.hashes(x)
runtime.Statics.finalizeHash(hashes.foldLeft(-889275714)(runtime.Statics.mix), hashes.length)
private def instance[A](f: A => List[Int], g: (A, A) => Boolean): HashBuilder[A] =
new HashBuilder[A] {
def hashes(x: A) = f(x)
def eqv(x: A, y: A) = g(x, y)
}
}

override def eqv(x: A, y: A): Boolean = builder.eqv(x, y)
}

implicit def emptyCoproductDerivedHash: MkHash[CNil] = null
// used when Hash[V] (a member of the coproduct) has to be derived.
implicit def coproductDerivedHash[L, R <: Coproduct](
implicit
hashV: Hash[L] OrElse MkHash[L],
hashT: MkHash[R]): MkHash[L :+: R] = new MkHash[L :+: R] {
def hash(value: L :+: R): Int = value match {
case Inl(l) => hashV.unify.hash(l)
case Inr(r) => hashT.hash(r)
}

def eqv(x: L :+: R, y: L :+: R): Boolean =
(x, y) match {
case (Inl(xl), Inl(yl)) => hashV.unify.eqv(xl, yl)
case (Inr(xr), Inr(yr)) => hashT.eqv(xr, yr)
case _ => false
}

}
private[derived] abstract class MkHashDerivation extends MkHashGenericProduct {
import shapeless._

implicit val mkHashCNil: MkHash[CNil] =
instance(_ => 0, (_, _) => true)

implicit def mkHashCCons[L, R <: Coproduct](
implicit L: Hash[L] OrElse MkHash[L], R: MkHash[R]
): MkHash[L :+: R] = instance({
case Inl(l) => L.unify.hash(l)
case Inr(r) => R.hash(r)
}, {
case (Inl(lx), Inl(ly)) => L.unify.eqv(lx, ly)
case (Inr(rx), Inr(ry)) => R.eqv(rx, ry)
case _ => false
})

implicit def mkHashCaseObject[A](implicit A: Generic.Aux[A, HNil]): MkHash[A] =
instance(_.hashCode, (_, _) => true)
}

implicit def genericDerivedHash[A, R](
implicit gen: Generic.Aux[A, R],
s: Lazy[MkHash[R]]): MkHash[A] = new MkHash[A] {
def hash(a: A): Int = s.value.hash(gen.to(a))
private[derived] abstract class MkHashGenericProduct extends MkHashGeneric {
import shapeless._

def eqv(x: A, y: A): Boolean = s.value.eqv(gen.to(x), gen.to(y))
}
implicit def mkHashGenericProduct[A, R <: HList](
implicit A: Generic.Aux[A, R], R: Lazy[HashBuilder[R]], ev: A <:< Product
): MkHash[A] = instance(
x => R.value.hash(A.to(x), util.VersionSpecific.productSeed(x)),
(x, y) => R.value.eqv(A.to(x), A.to(y))
)
}

private[derived] abstract class MkHashGeneric {
import shapeless._

implicit def mkHashGenericHList[A, R <: HList](
implicit A: Generic.Aux[A, R], R: Lazy[HashBuilder[R]]
): MkHash[A] = instance(
x => R.value.hash(A.to(x), MurmurHash3.productSeed),
(x, y) => R.value.eqv(A.to(x), A.to(y))
)

implicit def mkHashGenericCoproduct[A, R <: Coproduct](
implicit A: Generic.Aux[A, R], R: Lazy[MkHash[R]]
): MkHash[A] = instance(
x => R.value.hash(A.to(x)),
(x, y) => R.value.eqv(A.to(x), A.to(y))
)

protected def instance[A](f: A => Int, g: (A, A) => Boolean): MkHash[A] =
new MkHash[A] {
def hash(x: A) = f(x)
def eqv(x: A, y: A) = g(x, y)
}
}
10 changes: 5 additions & 5 deletions core/src/main/scala/cats/derived/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ object auto {

object hash {
implicit def kittensMkHash[A](
implicit refute: Refute[Hash[A]], hash: MkHash[A]
): Hash[A] = hash
implicit refute: Refute[Hash[A]], ev: Lazy[MkHash[A]]
): Hash[A] = ev.value
}

object functor {
Expand Down Expand Up @@ -174,8 +174,8 @@ object cached {

object hash {
implicit def kittensMkHash[A](
implicit refute: Refute[Hash[A]], ord: Cached[MkHash[A]])
: Hash[A] = ord.value
implicit refute: Refute[Hash[A]], cached: Cached[MkHash[A]]
): Hash[A] = cached.value
}

object functor {
Expand Down Expand Up @@ -294,7 +294,7 @@ object semi {

def order[A](implicit ev: Lazy[MkOrder[A]]): Order[A] = ev.value

def hash[A](implicit ev: MkHash[A]): Hash[A] = ev
def hash[A](implicit ev: Lazy[MkHash[A]]): Hash[A] = ev.value

def functor[F[_]](implicit F: Lazy[MkFunctor[F]]): Functor[F] = F.value

Expand Down
4 changes: 2 additions & 2 deletions core/src/test/scala/cats/derived/KittensSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package cats.derived

import cats.syntax.AllSyntax
import org.scalatest.FunSuite
import org.scalatest.funsuite.AnyFunSuite
import org.typelevel.discipline.scalatest.Discipline

import scala.util.control.NonFatal
Expand All @@ -28,7 +28,7 @@ import scala.util.control.NonFatal
* CatsSuite in the Cat project, this trait does not mix in any
* instances.
*/
trait KittensSuite extends FunSuite with Discipline with AllSyntax with SerializableTest
trait KittensSuite extends AnyFunSuite with Discipline with AllSyntax with SerializableTest


trait SerializableTest {
Expand Down
7 changes: 3 additions & 4 deletions core/src/test/scala/cats/derived/consk.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@
package cats.derived

import alleycats.ConsK
import org.scalacheck.Arbitrary
import org.scalatest.prop.GeneratorDrivenPropertyChecks
import org.scalatest.prop.{Generator, GeneratorDrivenPropertyChecks}

class ConsKSuite extends KittensSuite with GeneratorDrivenPropertyChecks {
import TestDefns._

def checkConsK[F[_], A: Arbitrary](nil: F[A])(fromSeq: Seq[A] => F[A])(implicit F: ConsK[F]): Unit =
forAll((xs: Seq[A]) => assert(xs.foldRight(nil)(F.cons) == fromSeq(xs)))
def checkConsK[F[_], A: Generator](nil: F[A])(fromSeq: Seq[A] => F[A])(implicit F: ConsK[F]): Unit =
forAll((xs: List[A]) => assert(xs.foldRight(nil)(F.cons) == fromSeq(xs)))

def testConsK(context: String)(implicit iList: ConsK[IList], snoc: ConsK[Snoc]): Unit = {
test(s"$context.ConsK[IList]")(checkConsK[IList, Int](INil())(IList.fromSeq))
Expand Down
Loading

0 comments on commit 65c637b

Please sign in to comment.