Skip to content

Commit

Permalink
Scala 3 Support, helper syntax for MapK.apply, override equals/hashCode
Browse files Browse the repository at this point in the history
  • Loading branch information
dylemma committed Jun 6, 2021
1 parent 0ed75e3 commit 4ca21b9
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 54 deletions.
27 changes: 17 additions & 10 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -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
}
}
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.4.3
sbt.version=1.5.2
4 changes: 4 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 43 additions & 24 deletions src/main/scala/com/codedx/util/MapK.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
}
Expand All @@ -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]] {
Expand All @@ -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)
}
}
}
27 changes: 17 additions & 10 deletions src/main/scala/com/codedx/util/MutableMapK.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand All @@ -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)
Expand Down
46 changes: 39 additions & 7 deletions src/test/scala/com/codedx/util/MapKSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
22 changes: 20 additions & 2 deletions src/test/scala/com/codedx/util/MutableMapKSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 4ca21b9

Please sign in to comment.