From 4ca21b90f3cfcc859b327a44442e3e5d0a42e1cd Mon Sep 17 00:00:00 2001 From: Dylan Halperin Date: Sun, 6 Jun 2021 09:23:56 -0400 Subject: [PATCH] Scala 3 Support, helper syntax for MapK.apply, override equals/hashCode --- build.sbt | 27 +++++--- project/build.properties | 2 +- readme.md | 4 ++ src/main/scala/com/codedx/util/MapK.scala | 67 ++++++++++++------- .../scala/com/codedx/util/MutableMapK.scala | 27 +++++--- src/test/scala/com/codedx/util/MapKSpec.scala | 46 +++++++++++-- .../com/codedx/util/MutableMapKSpec.scala | 22 +++++- 7 files changed, 141 insertions(+), 54 deletions(-) diff --git a/build.sbt b/build.sbt index a901e59..3c1a984 100644 --- a/build.sbt +++ b/build.sbt @@ -1,15 +1,22 @@ name := "mapk" -version := "1.1.0" +version := "1.2.0" -scalaVersion := "2.12.13" -crossScalaVersions := List("2.12.13", "2.13.5") -scalacOptions := List("-deprecation", "-unchecked", "-feature", "-language:higherKinds") -scalacOptions ++= (scalaBinaryVersion.value match { - case "2.12" => Seq("-Ypartial-unification") +scalaVersion := "2.12.14" +crossScalaVersions := List("2.12.14", "2.13.6", "3.0.0") + +libraryDependencies += "org.typelevel" %% "cats-core" % "2.6.1" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.9" % "test" +libraryDependencies ++= (scalaBinaryVersion.value match { + case "2.12" | "2.13" => compilerPlugin("org.typelevel" % "kind-projector" % "0.13.0" cross CrossVersion.full) :: Nil case _ => Nil }) -libraryDependencies += "org.typelevel" %% "cats-core" % "2.4.2" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.5" % "test" - -addCompilerPlugin("org.typelevel" % "kind-projector" % "0.11.3" cross CrossVersion.full) +scalacOptions := List("-deprecation", "-unchecked", "-feature", "-language:higherKinds") +scalacOptions ++= { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, 12)) => Seq("-Ypartial-unification", "-Xsource:3") + case Some((2, 13)) => Seq("-Xsource:3") + case Some((3, _)) => Seq("-Ykind-projector") + case _ => Nil + } +} diff --git a/project/build.properties b/project/build.properties index 947bdd3..19479ba 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.4.3 +sbt.version=1.5.2 diff --git a/readme.md b/readme.md index 009037d..c876b53 100644 --- a/readme.md +++ b/readme.md @@ -16,6 +16,10 @@ val info: MapK[MyKey, cats.Id] = MapK.empty[MyKey, cats.Id] .updated(Age, 21) .updated(Name, "Dylan") +// alternate apply syntax: +import MapK.entrySyntax._ +val info = MapK(Age ~>> 21, Name ~>> "Dylan") + val age: Option[Int] = info.get(Age) // Some(21) val name: Option[String] = info.get(Name) // Some("Dylan") val numThings: Option[Int] = info.get(NumThings) // None diff --git a/src/main/scala/com/codedx/util/MapK.scala b/src/main/scala/com/codedx/util/MapK.scala index 02a39e6..0a5ec57 100644 --- a/src/main/scala/com/codedx/util/MapK.scala +++ b/src/main/scala/com/codedx/util/MapK.scala @@ -21,52 +21,60 @@ import cats.arrow.FunctionK import cats.data.Tuple2K import cats.{ Monoid, SemigroupK, ~> } -class MapK[K[_], V[_]] private[MapK](val untyped: Map[K[_], V[_]]) { self => +class MapK[K[*], V[*]] private[MapK](val untyped: Map[K[Any], V[Any]]) { self => type Entry[x] = (K[x], V[x]) type Combiner[x] = (V[x], V[x]) => V[x] - def contains(key: K[_]): Boolean = untyped.contains(key) - def get[T](key: K[T]): Option[V[T]] = untyped.get(key).map(_.asInstanceOf[V[T]]) - def apply[T](key: K[T]): V[T] = untyped(key).asInstanceOf[V[T]] - def +[T](key: K[T], value: V[T]): MapK[K, V] = new MapK[K, V](untyped + (key -> value)) + override def equals(obj: Any): Boolean = obj match { + case that: MapK[?, ?] => this.untyped == that.untyped + case _ => false + } + override def hashCode(): Int = untyped.hashCode() + + @inline private def asAny[X[_], A](x: X[A]): X[Any] = x.asInstanceOf[X[Any]] + + def contains[A](key: K[A]): Boolean = untyped.contains(asAny(key)) + def get[T](key: K[T]): Option[V[T]] = untyped.get(asAny(key)).map(_.asInstanceOf[V[T]]) + def apply[T](key: K[T]): V[T] = untyped(asAny(key)).asInstanceOf[V[T]] + def +[T](key: K[T], value: V[T]): MapK[K, V] = new MapK[K, V](untyped + (asAny(key) -> asAny(value))) def updated[T](key: K[T], value: V[T]): MapK[K, V] = this.+(key, value) def ++(that: MapK[K, V]): MapK[K, V] = { - val out = Map.newBuilder[K[_], V[_]] - val add = new FunctionK[Entry, Lambda[x => Unit]] { - def apply[A](fa: (K[A], V[A])): Unit = out += fa + val out = Map.newBuilder[K[Any], V[Any]] + val add = new FunctionK[Entry, ({ type U[x] = Unit })#U] { + def apply[A](fa: (K[A], V[A])): Unit = out += (asAny(fa._1) -> asAny(fa._2)) } this.foreach(add) that.foreach(add) new MapK[K, V](out.result()) } - def foreach(f: Entry ~> Lambda[x => Unit]): Unit = { - for ((key, value) <- untyped) { - val t = MapK.tuple(key, value) - f(t) - } + def foreach(f: Entry ~> ({ type U[x] = Unit })#U): Unit = { + def handleKv[A](kv: (K[Any], V[Any])) = f(MapK.tuple(kv._1.asInstanceOf[K[A]], kv._2.asInstanceOf[V[A]])) + for (kv <- untyped) handleKv(kv) } def mapValues[V2[_]](f: V ~> V2): MapK[K, V2] = { - val mapped = Map.newBuilder[K[_], V2[_]] - for((k, v) <- untyped) mapped += (k -> f(v)) + val mapped = Map.newBuilder[K[Any], V2[Any]] + def handleKv[A](kv: (K[Any], V[Any])): Unit = mapped += (kv._1.asInstanceOf[K[Any]] -> asAny(f(kv._2.asInstanceOf[V[A]]))) + for(kv <- untyped) handleKv(kv) MapK.coerce[K, V2](mapped.result()) } def map[V2[_]](f: Entry ~> V2): MapK[K, V2] = { - val mapped = Map.newBuilder[K[_], V2[_]] - for((k, v) <- untyped) mapped += (k -> f(MapK.tuple(k, v))) + val mapped = Map.newBuilder[K[Any], V2[Any]] + def handleKv[A](kv: (K[Any], V[Any])) = mapped += (kv._1.asInstanceOf[K[Any]] -> f(MapK.tuple[K, V, A](kv._1.asInstanceOf[K[A]], kv._2.asInstanceOf[V[A]])).asInstanceOf[V2[Any]]) + for(kv <- untyped) handleKv(kv) MapK.coerce[K, V2](mapped.result()) } - def keys: Iterable[K[_]] = untyped.keys + def keys: Iterable[K[Any]] = untyped.keys def merge(that: MapK[K, V], combiner: K ~> Combiner): MapK[K, V] = { var out: MapK[K, V] = self - that.foreach(new FunctionK[that.Entry, Lambda[x => Unit]] { + that.foreach(new FunctionK[that.Entry, ({ type U[x] = Unit })#U] { def apply[A](kv: (K[A], V[A])): Unit = { val (k, newValue) = kv val v2 = self.get(k) match { case Some(oldValue) => combiner(k)(oldValue, newValue) case None => newValue } - out += (k, v2) + out = out + (k, v2) } }) out @@ -79,7 +87,7 @@ class MapK[K[_], V[_]] private[MapK](val untyped: Map[K[_], V[_]]) { self => override def toString() = { val sb = new StringBuilder() sb append "TypedMap {\n" - foreach(new FunctionK[Entry, Lambda[x => Unit]] { + foreach(new FunctionK[Entry, ({ type U[x] = Unit })#U] { def apply[A](entry: Entry[A]): Unit = { sb append " " append entry._1.toString append ": " append entry._2.toString append "\n" } @@ -93,12 +101,14 @@ object MapK { private[util] def tuple[K[_], V[_], T](key: K[T], value: Any): (K[T], V[T]) = (key, value.asInstanceOf[V[T]]) - def coerce[K[_], V[_]](map: Map[K[_], V[_]]): MapK[K, V] = new MapK[K, V](map) + def coerce[K[_], V[_]](map: Map[K[Any], V[Any]]): MapK[K, V] = new MapK[K, V](map) def empty[K[_], V[_]]: MapK[K, V] = new MapK[K, V](Map.empty) - def apply[K[_], V[_]](entries: Tuple2K[K, V, _]*): MapK[K, V] = coerce[K, V] { - entries.view.map { t => (t.first, t.second) }.toMap + def apply[K[_], V[_]](entries: Tuple2K[K, V, ?]*): MapK[K, V] = coerce[K, V] { + val mb = Map.newBuilder[K[Any], V[Any]] + for (t <- entries) mb += (t.first.asInstanceOf[K[Any]] -> t.second.asInstanceOf[V[Any]]) + mb.result() } implicit def catsMonoidForMapK[K[_], V[_]](implicit V: SemigroupK[V]): Monoid[MapK[K, V]] = new Monoid[MapK[K, V]] { @@ -110,4 +120,13 @@ object MapK { x.merge(y, getCombiner) } } + + /** Convenience syntax for constructing `Tuple2K` instances to use with `MapK.apply` + */ + object entrySyntax { + implicit class RichKey[K[_], A](key: K[A]) { + def ~>[V[_]](value: V[A]) = Tuple2K[K, V, A](key, value) + def ~>>(value: A) = Tuple2K[K, cats.Id, A](key, value) + } + } } diff --git a/src/main/scala/com/codedx/util/MutableMapK.scala b/src/main/scala/com/codedx/util/MutableMapK.scala index 2cda0cd..d164b53 100644 --- a/src/main/scala/com/codedx/util/MutableMapK.scala +++ b/src/main/scala/com/codedx/util/MutableMapK.scala @@ -28,24 +28,30 @@ import cats.~> * @tparam K * @tparam V */ -class MutableMapK[K[_], V[_]] private(private val inner: MutableMap[K[_], V[_]]) { self => +class MutableMapK[K[*], V[*]] private(private val inner: MutableMap[K[Any], V[Any]]) { self => def this() = this(MutableMap.empty) + override def hashCode(): Int = inner.hashCode() + override def equals(obj: Any): Boolean = obj match { + case m: MutableMapK[_, _] => m.inner == inner + case _ => false + } + type Entry[x] = (K[x], V[x]) def entry[T](key: K[T], value: V[T]): Entry[T] = (key, value) override def toString = inner.toString - def contains(key: K[_]): Boolean = inner.contains(key) - def get[T](key: K[T]): Option[V[T]] = inner.get(key).map(_.asInstanceOf[V[T]]) - def apply[T](key: K[T]): V[T] = inner(key).asInstanceOf[V[T]] + def contains[T](key: K[T]): Boolean = inner.contains(key.asInstanceOf[K[Any]]) + def get[T](key: K[T]): Option[V[T]] = inner.get(key.asInstanceOf[K[Any]]).map(_.asInstanceOf[V[T]]) + def apply[T](key: K[T]): V[T] = inner(key.asInstanceOf[K[Any]]).asInstanceOf[V[T]] def add[T](key: K[T], value: V[T]): this.type = { - inner.put(key, value) + inner.put(key.asInstanceOf[K[Any]], value.asInstanceOf[V[Any]]) this } def add[T](entry: Entry[T]): this.type = add(entry._1, entry._2) def remove[T](key: K[T]): Option[V[T]] = { - inner.remove(key).map(_.asInstanceOf[V[T]]) + inner.remove(key.asInstanceOf[K[Any]]).map(_.asInstanceOf[V[T]]) } def getOrElseUpdate[T](key: K[T], value: => V[T]) = get(key) match { case None => @@ -57,20 +63,21 @@ class MutableMapK[K[_], V[_]] private(private val inner: MutableMap[K[_], V[_]]) } def mapValues[U[_]](f: V ~> U) = { val mapped = new MutableMapK[K, U] - this.foreach(new FunctionK[Entry, Lambda[x => Unit]] { + this.foreach(new FunctionK[Entry, ({ type U[x] = Unit })#U] { def apply[A](kv: (K[A], V[A])): Unit = mapped.add(kv._1, f(kv._2)) }) mapped } def addFrom(that: MutableMapK[K, V]): this.type = { - that.foreach(new FunctionK[Entry, Lambda[x => Unit]] { + that.foreach(new FunctionK[Entry, ({ type U[x] = Unit })#U] { def apply[A](kv: (K[A], V[A])): Unit = self.add(kv._1, kv._2) }) this } def keys = inner.keySet - def foreach(f: Entry ~> Lambda[x => Unit]) = { - for ((key, value) <- inner) f(MapK.tuple(key, value)) + def foreach(f: Entry ~> ({ type U[x] = Unit })#U) = { + def handleKv[A](kv: (K[Any], V[Any])) = f(MapK.tuple(kv._1.asInstanceOf[K[A]], kv._2.asInstanceOf[K[A]])) + for (kv <- inner) handleKv(kv) } def toMap: MapK[K, V] = MapK.coerce[K, V](inner.toMap) diff --git a/src/test/scala/com/codedx/util/MapKSpec.scala b/src/test/scala/com/codedx/util/MapKSpec.scala index a567498..8cf6b8f 100644 --- a/src/test/scala/com/codedx/util/MapKSpec.scala +++ b/src/test/scala/com/codedx/util/MapKSpec.scala @@ -46,6 +46,34 @@ class MapKSpec extends AnyFunSpec with Matchers { .updated(Name, List("a", "b")) describe("MapK") { + describe("entrySyntax") { + import MapK.entrySyntax._ + it ("should allow for a convenient MapK.apply with cats.Id value types") { + val appliedMap = MapK(Age ~>> 32, Name ~>> "Dylan", Fingers ~>> 10) + appliedMap shouldEqual dylan + } + it ("should allow for a convenient MapK.apply with higher-kinded value types") { + val appliedMap = MapK(Age ~> List(1, 2, 3, 4), Name ~> List("a", "b")) + appliedMap shouldEqual multi + } + } + + describe("equals and hashCode") { + import MapK.entrySyntax._ + it ("should behave properly for identical MapK instances") { + val a = MapK(Age ~>> 21, Name ~>> "John Doe", Fingers ~>> 10) + val b = MapK(Age ~>> 21, Name ~>> "John Doe", Fingers ~>> 10) + a.equals(b) shouldBe true + a.hashCode shouldEqual b.hashCode + } + it ("should behave properly for non-identical MapK instances") { + val a = MapK(Age ~>> 21, Name ~>> "John Doe") + val b = MapK(Age ~>> 21, Name ~>> "John Doe", Fingers ~>> 10) + a.equals(b) shouldBe false + a.hashCode should not equal b.hashCode + } + } + describe(".untyped") { it("should return a regular Map containing the same entries, minus the explicit type information") { val u = ezio.untyped @@ -139,7 +167,7 @@ class MapKSpec extends AnyFunSpec with Matchers { it ("should pass each entry to the callback function, in no particular order") { val seenValuesB = List.newBuilder[Any] val seenKeysB = Set.newBuilder[MyKey[_]] - ezio.foreach(new FunctionK[ezio.Entry, Lambda[x => Unit]] { + ezio.foreach(new FunctionK[ezio.Entry, ({ type F[x] = Unit })#F] { def apply[A](fa: (MyKey[A], Id[A])) = { seenKeysB += fa._1 seenValuesB += fa._2 @@ -152,7 +180,9 @@ class MapKSpec extends AnyFunSpec with Matchers { describe(".mapValues(f)") { it ("should create a new MapK whose values correspond to the original map values, transformed by f") { - val ezioList = ezio.mapValues(Lambda[cats.Id ~> List](v => List(v, v))) + val ezioList = ezio.mapValues(new FunctionK[cats.Id, List] { + def apply[A](a: A) = List(a, a) + }) ezioList(Name) shouldEqual List("Ezio Auditore", "Ezio Auditore") ezioList(Age) shouldEqual List(65, 65) ezioList(Fingers) shouldEqual List(9, 9) @@ -161,9 +191,11 @@ class MapKSpec extends AnyFunSpec with Matchers { describe(".map(f)") { it ("should create a new MapK whose entries correspond to the original map entries, transformed by f") { - val restoreFinger = Lambda[ezio.Entry ~> cats.Id] { - case (Fingers, n) => n + 1 - case (k, v) => v + val restoreFinger = new FunctionK[ezio.Entry, cats.Id] { + def apply[A](entry: ezio.Entry[A]) = entry match { + case (Fingers, n) => n + 1 + case (k, v) => v + } } val ezio2 = ezio.map(restoreFinger) ezio(Fingers) shouldEqual 9 @@ -191,8 +223,8 @@ class MapKSpec extends AnyFunSpec with Matchers { val max = new FunctionK[MyKey, MapK[MyKey, cats.Id]#Combiner] { def apply[A](fa: MyKey[A]) = fa match { case Name => (l: String, r: String) => (if (l.compareToIgnoreCase(r) > 0) l else r): String - case Age => _ max _ - case Fingers => _ max _ + case Age => (l: Int, r: Int) => l max r + case Fingers => (l: Int, r: Int) => l max r } } val merged1 = ezio.merge(dylan, max) diff --git a/src/test/scala/com/codedx/util/MutableMapKSpec.scala b/src/test/scala/com/codedx/util/MutableMapKSpec.scala index 1d82a29..66d21a3 100644 --- a/src/test/scala/com/codedx/util/MutableMapKSpec.scala +++ b/src/test/scala/com/codedx/util/MutableMapKSpec.scala @@ -34,6 +34,22 @@ class MutableMapKSpec extends AnyFunSpec with Matchers{ } describe("MutableMapK") { + describe("equals and hashCode") { + it ("should behave properly for identical MutableMapK instances") { + val a = makeDemoMap + val b = makeDemoMap + a.equals(b) shouldBe true + a.hashCode shouldEqual b.hashCode + } + it ("should behave properly for non-identical MutableMapK instances") { + val a = makeDemoMap + val b = makeDemoMap + b.add(Age, 20) + a.equals(b) shouldBe false + a.hashCode should not equal b.hashCode + } + } + describe(".contains(key)") { it ("should return whether or not the map contains the given key") { makeDemoMap.contains(Extra) shouldBe false @@ -112,7 +128,9 @@ class MutableMapKSpec extends AnyFunSpec with Matchers{ describe(".mapValues(f)") { it ("should create a new MutableMapK whose values correspond to the original map values, transformed by f") { - val mapped: MutableMapK[MyKey, List] = makeDemoMap.mapValues(Lambda[cats.Id ~> List](v => List(v, v))) + val mapped: MutableMapK[MyKey, List] = makeDemoMap.mapValues(new FunctionK[cats.Id, List] { + def apply[A](v: A) = List(v, v) + }) mapped(Name) shouldEqual List("demo", "demo") mapped(Age) shouldEqual List(10, 10) } @@ -154,7 +172,7 @@ class MutableMapKSpec extends AnyFunSpec with Matchers{ val seenValuesB = List.newBuilder[Any] val seenKeysB = Set.newBuilder[MyKey[_]] val m = makeDemoMap - m.foreach(new FunctionK[m.Entry, Lambda[x => Unit]] { + m.foreach(new FunctionK[m.Entry, ({ type F[x] = Unit })#F] { def apply[A](fa: (MyKey[A], Id[A])) = { seenKeysB += fa._1 seenValuesB += fa._2