Skip to content

Commit 4ca21b9

Browse files
committed
Scala 3 Support, helper syntax for MapK.apply, override equals/hashCode
1 parent 0ed75e3 commit 4ca21b9

File tree

7 files changed

+141
-54
lines changed

7 files changed

+141
-54
lines changed

build.sbt

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
name := "mapk"
2-
version := "1.1.0"
2+
version := "1.2.0"
33

4-
scalaVersion := "2.12.13"
5-
crossScalaVersions := List("2.12.13", "2.13.5")
6-
scalacOptions := List("-deprecation", "-unchecked", "-feature", "-language:higherKinds")
7-
scalacOptions ++= (scalaBinaryVersion.value match {
8-
case "2.12" => Seq("-Ypartial-unification")
4+
scalaVersion := "2.12.14"
5+
crossScalaVersions := List("2.12.14", "2.13.6", "3.0.0")
6+
7+
libraryDependencies += "org.typelevel" %% "cats-core" % "2.6.1"
8+
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.9" % "test"
9+
libraryDependencies ++= (scalaBinaryVersion.value match {
10+
case "2.12" | "2.13" => compilerPlugin("org.typelevel" % "kind-projector" % "0.13.0" cross CrossVersion.full) :: Nil
911
case _ => Nil
1012
})
1113

12-
libraryDependencies += "org.typelevel" %% "cats-core" % "2.4.2"
13-
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.5" % "test"
14-
15-
addCompilerPlugin("org.typelevel" % "kind-projector" % "0.11.3" cross CrossVersion.full)
14+
scalacOptions := List("-deprecation", "-unchecked", "-feature", "-language:higherKinds")
15+
scalacOptions ++= {
16+
CrossVersion.partialVersion(scalaVersion.value) match {
17+
case Some((2, 12)) => Seq("-Ypartial-unification", "-Xsource:3")
18+
case Some((2, 13)) => Seq("-Xsource:3")
19+
case Some((3, _)) => Seq("-Ykind-projector")
20+
case _ => Nil
21+
}
22+
}

project/build.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
sbt.version=1.4.3
1+
sbt.version=1.5.2

readme.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ val info: MapK[MyKey, cats.Id] = MapK.empty[MyKey, cats.Id]
1616
.updated(Age, 21)
1717
.updated(Name, "Dylan")
1818

19+
// alternate apply syntax:
20+
import MapK.entrySyntax._
21+
val info = MapK(Age ~>> 21, Name ~>> "Dylan")
22+
1923
val age: Option[Int] = info.get(Age) // Some(21)
2024
val name: Option[String] = info.get(Name) // Some("Dylan")
2125
val numThings: Option[Int] = info.get(NumThings) // None

src/main/scala/com/codedx/util/MapK.scala

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,52 +21,60 @@ import cats.arrow.FunctionK
2121
import cats.data.Tuple2K
2222
import cats.{ Monoid, SemigroupK, ~> }
2323

24-
class MapK[K[_], V[_]] private[MapK](val untyped: Map[K[_], V[_]]) { self =>
24+
class MapK[K[*], V[*]] private[MapK](val untyped: Map[K[Any], V[Any]]) { self =>
2525
type Entry[x] = (K[x], V[x])
2626
type Combiner[x] = (V[x], V[x]) => V[x]
2727

28-
def contains(key: K[_]): Boolean = untyped.contains(key)
29-
def get[T](key: K[T]): Option[V[T]] = untyped.get(key).map(_.asInstanceOf[V[T]])
30-
def apply[T](key: K[T]): V[T] = untyped(key).asInstanceOf[V[T]]
31-
def +[T](key: K[T], value: V[T]): MapK[K, V] = new MapK[K, V](untyped + (key -> value))
28+
override def equals(obj: Any): Boolean = obj match {
29+
case that: MapK[?, ?] => this.untyped == that.untyped
30+
case _ => false
31+
}
32+
override def hashCode(): Int = untyped.hashCode()
33+
34+
@inline private def asAny[X[_], A](x: X[A]): X[Any] = x.asInstanceOf[X[Any]]
35+
36+
def contains[A](key: K[A]): Boolean = untyped.contains(asAny(key))
37+
def get[T](key: K[T]): Option[V[T]] = untyped.get(asAny(key)).map(_.asInstanceOf[V[T]])
38+
def apply[T](key: K[T]): V[T] = untyped(asAny(key)).asInstanceOf[V[T]]
39+
def +[T](key: K[T], value: V[T]): MapK[K, V] = new MapK[K, V](untyped + (asAny(key) -> asAny(value)))
3240
def updated[T](key: K[T], value: V[T]): MapK[K, V] = this.+(key, value)
3341
def ++(that: MapK[K, V]): MapK[K, V] = {
34-
val out = Map.newBuilder[K[_], V[_]]
35-
val add = new FunctionK[Entry, Lambda[x => Unit]] {
36-
def apply[A](fa: (K[A], V[A])): Unit = out += fa
42+
val out = Map.newBuilder[K[Any], V[Any]]
43+
val add = new FunctionK[Entry, ({ type U[x] = Unit })#U] {
44+
def apply[A](fa: (K[A], V[A])): Unit = out += (asAny(fa._1) -> asAny(fa._2))
3745
}
3846
this.foreach(add)
3947
that.foreach(add)
4048
new MapK[K, V](out.result())
4149
}
42-
def foreach(f: Entry ~> Lambda[x => Unit]): Unit = {
43-
for ((key, value) <- untyped) {
44-
val t = MapK.tuple(key, value)
45-
f(t)
46-
}
50+
def foreach(f: Entry ~> ({ type U[x] = Unit })#U): Unit = {
51+
def handleKv[A](kv: (K[Any], V[Any])) = f(MapK.tuple(kv._1.asInstanceOf[K[A]], kv._2.asInstanceOf[V[A]]))
52+
for (kv <- untyped) handleKv(kv)
4753
}
4854
def mapValues[V2[_]](f: V ~> V2): MapK[K, V2] = {
49-
val mapped = Map.newBuilder[K[_], V2[_]]
50-
for((k, v) <- untyped) mapped += (k -> f(v))
55+
val mapped = Map.newBuilder[K[Any], V2[Any]]
56+
def handleKv[A](kv: (K[Any], V[Any])): Unit = mapped += (kv._1.asInstanceOf[K[Any]] -> asAny(f(kv._2.asInstanceOf[V[A]])))
57+
for(kv <- untyped) handleKv(kv)
5158
MapK.coerce[K, V2](mapped.result())
5259
}
5360
def map[V2[_]](f: Entry ~> V2): MapK[K, V2] = {
54-
val mapped = Map.newBuilder[K[_], V2[_]]
55-
for((k, v) <- untyped) mapped += (k -> f(MapK.tuple(k, v)))
61+
val mapped = Map.newBuilder[K[Any], V2[Any]]
62+
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]])
63+
for(kv <- untyped) handleKv(kv)
5664
MapK.coerce[K, V2](mapped.result())
5765
}
58-
def keys: Iterable[K[_]] = untyped.keys
66+
def keys: Iterable[K[Any]] = untyped.keys
5967

6068
def merge(that: MapK[K, V], combiner: K ~> Combiner): MapK[K, V] = {
6169
var out: MapK[K, V] = self
62-
that.foreach(new FunctionK[that.Entry, Lambda[x => Unit]] {
70+
that.foreach(new FunctionK[that.Entry, ({ type U[x] = Unit })#U] {
6371
def apply[A](kv: (K[A], V[A])): Unit = {
6472
val (k, newValue) = kv
6573
val v2 = self.get(k) match {
6674
case Some(oldValue) => combiner(k)(oldValue, newValue)
6775
case None => newValue
6876
}
69-
out += (k, v2)
77+
out = out + (k, v2)
7078
}
7179
})
7280
out
@@ -79,7 +87,7 @@ class MapK[K[_], V[_]] private[MapK](val untyped: Map[K[_], V[_]]) { self =>
7987
override def toString() = {
8088
val sb = new StringBuilder()
8189
sb append "TypedMap {\n"
82-
foreach(new FunctionK[Entry, Lambda[x => Unit]] {
90+
foreach(new FunctionK[Entry, ({ type U[x] = Unit })#U] {
8391
def apply[A](entry: Entry[A]): Unit = {
8492
sb append " " append entry._1.toString append ": " append entry._2.toString append "\n"
8593
}
@@ -93,12 +101,14 @@ object MapK {
93101

94102
private[util] def tuple[K[_], V[_], T](key: K[T], value: Any): (K[T], V[T]) = (key, value.asInstanceOf[V[T]])
95103

96-
def coerce[K[_], V[_]](map: Map[K[_], V[_]]): MapK[K, V] = new MapK[K, V](map)
104+
def coerce[K[_], V[_]](map: Map[K[Any], V[Any]]): MapK[K, V] = new MapK[K, V](map)
97105

98106
def empty[K[_], V[_]]: MapK[K, V] = new MapK[K, V](Map.empty)
99107

100-
def apply[K[_], V[_]](entries: Tuple2K[K, V, _]*): MapK[K, V] = coerce[K, V] {
101-
entries.view.map { t => (t.first, t.second) }.toMap
108+
def apply[K[_], V[_]](entries: Tuple2K[K, V, ?]*): MapK[K, V] = coerce[K, V] {
109+
val mb = Map.newBuilder[K[Any], V[Any]]
110+
for (t <- entries) mb += (t.first.asInstanceOf[K[Any]] -> t.second.asInstanceOf[V[Any]])
111+
mb.result()
102112
}
103113

104114
implicit def catsMonoidForMapK[K[_], V[_]](implicit V: SemigroupK[V]): Monoid[MapK[K, V]] = new Monoid[MapK[K, V]] {
@@ -110,4 +120,13 @@ object MapK {
110120
x.merge(y, getCombiner)
111121
}
112122
}
123+
124+
/** Convenience syntax for constructing `Tuple2K` instances to use with `MapK.apply`
125+
*/
126+
object entrySyntax {
127+
implicit class RichKey[K[_], A](key: K[A]) {
128+
def ~>[V[_]](value: V[A]) = Tuple2K[K, V, A](key, value)
129+
def ~>>(value: A) = Tuple2K[K, cats.Id, A](key, value)
130+
}
131+
}
113132
}

src/main/scala/com/codedx/util/MutableMapK.scala

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,30 @@ import cats.~>
2828
* @tparam K
2929
* @tparam V
3030
*/
31-
class MutableMapK[K[_], V[_]] private(private val inner: MutableMap[K[_], V[_]]) { self =>
31+
class MutableMapK[K[*], V[*]] private(private val inner: MutableMap[K[Any], V[Any]]) { self =>
3232
def this() = this(MutableMap.empty)
3333

34+
override def hashCode(): Int = inner.hashCode()
35+
override def equals(obj: Any): Boolean = obj match {
36+
case m: MutableMapK[_, _] => m.inner == inner
37+
case _ => false
38+
}
39+
3440
type Entry[x] = (K[x], V[x])
3541
def entry[T](key: K[T], value: V[T]): Entry[T] = (key, value)
3642

3743
override def toString = inner.toString
3844

39-
def contains(key: K[_]): Boolean = inner.contains(key)
40-
def get[T](key: K[T]): Option[V[T]] = inner.get(key).map(_.asInstanceOf[V[T]])
41-
def apply[T](key: K[T]): V[T] = inner(key).asInstanceOf[V[T]]
45+
def contains[T](key: K[T]): Boolean = inner.contains(key.asInstanceOf[K[Any]])
46+
def get[T](key: K[T]): Option[V[T]] = inner.get(key.asInstanceOf[K[Any]]).map(_.asInstanceOf[V[T]])
47+
def apply[T](key: K[T]): V[T] = inner(key.asInstanceOf[K[Any]]).asInstanceOf[V[T]]
4248
def add[T](key: K[T], value: V[T]): this.type = {
43-
inner.put(key, value)
49+
inner.put(key.asInstanceOf[K[Any]], value.asInstanceOf[V[Any]])
4450
this
4551
}
4652
def add[T](entry: Entry[T]): this.type = add(entry._1, entry._2)
4753
def remove[T](key: K[T]): Option[V[T]] = {
48-
inner.remove(key).map(_.asInstanceOf[V[T]])
54+
inner.remove(key.asInstanceOf[K[Any]]).map(_.asInstanceOf[V[T]])
4955
}
5056
def getOrElseUpdate[T](key: K[T], value: => V[T]) = get(key) match {
5157
case None =>
@@ -57,20 +63,21 @@ class MutableMapK[K[_], V[_]] private(private val inner: MutableMap[K[_], V[_]])
5763
}
5864
def mapValues[U[_]](f: V ~> U) = {
5965
val mapped = new MutableMapK[K, U]
60-
this.foreach(new FunctionK[Entry, Lambda[x => Unit]] {
66+
this.foreach(new FunctionK[Entry, ({ type U[x] = Unit })#U] {
6167
def apply[A](kv: (K[A], V[A])): Unit = mapped.add(kv._1, f(kv._2))
6268
})
6369
mapped
6470
}
6571
def addFrom(that: MutableMapK[K, V]): this.type = {
66-
that.foreach(new FunctionK[Entry, Lambda[x => Unit]] {
72+
that.foreach(new FunctionK[Entry, ({ type U[x] = Unit })#U] {
6773
def apply[A](kv: (K[A], V[A])): Unit = self.add(kv._1, kv._2)
6874
})
6975
this
7076
}
7177
def keys = inner.keySet
72-
def foreach(f: Entry ~> Lambda[x => Unit]) = {
73-
for ((key, value) <- inner) f(MapK.tuple(key, value))
78+
def foreach(f: Entry ~> ({ type U[x] = Unit })#U) = {
79+
def handleKv[A](kv: (K[Any], V[Any])) = f(MapK.tuple(kv._1.asInstanceOf[K[A]], kv._2.asInstanceOf[K[A]]))
80+
for (kv <- inner) handleKv(kv)
7481
}
7582

7683
def toMap: MapK[K, V] = MapK.coerce[K, V](inner.toMap)

src/test/scala/com/codedx/util/MapKSpec.scala

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,34 @@ class MapKSpec extends AnyFunSpec with Matchers {
4646
.updated(Name, List("a", "b"))
4747

4848
describe("MapK") {
49+
describe("entrySyntax") {
50+
import MapK.entrySyntax._
51+
it ("should allow for a convenient MapK.apply with cats.Id value types") {
52+
val appliedMap = MapK(Age ~>> 32, Name ~>> "Dylan", Fingers ~>> 10)
53+
appliedMap shouldEqual dylan
54+
}
55+
it ("should allow for a convenient MapK.apply with higher-kinded value types") {
56+
val appliedMap = MapK(Age ~> List(1, 2, 3, 4), Name ~> List("a", "b"))
57+
appliedMap shouldEqual multi
58+
}
59+
}
60+
61+
describe("equals and hashCode") {
62+
import MapK.entrySyntax._
63+
it ("should behave properly for identical MapK instances") {
64+
val a = MapK(Age ~>> 21, Name ~>> "John Doe", Fingers ~>> 10)
65+
val b = MapK(Age ~>> 21, Name ~>> "John Doe", Fingers ~>> 10)
66+
a.equals(b) shouldBe true
67+
a.hashCode shouldEqual b.hashCode
68+
}
69+
it ("should behave properly for non-identical MapK instances") {
70+
val a = MapK(Age ~>> 21, Name ~>> "John Doe")
71+
val b = MapK(Age ~>> 21, Name ~>> "John Doe", Fingers ~>> 10)
72+
a.equals(b) shouldBe false
73+
a.hashCode should not equal b.hashCode
74+
}
75+
}
76+
4977
describe(".untyped") {
5078
it("should return a regular Map containing the same entries, minus the explicit type information") {
5179
val u = ezio.untyped
@@ -139,7 +167,7 @@ class MapKSpec extends AnyFunSpec with Matchers {
139167
it ("should pass each entry to the callback function, in no particular order") {
140168
val seenValuesB = List.newBuilder[Any]
141169
val seenKeysB = Set.newBuilder[MyKey[_]]
142-
ezio.foreach(new FunctionK[ezio.Entry, Lambda[x => Unit]] {
170+
ezio.foreach(new FunctionK[ezio.Entry, ({ type F[x] = Unit })#F] {
143171
def apply[A](fa: (MyKey[A], Id[A])) = {
144172
seenKeysB += fa._1
145173
seenValuesB += fa._2
@@ -152,7 +180,9 @@ class MapKSpec extends AnyFunSpec with Matchers {
152180

153181
describe(".mapValues(f)") {
154182
it ("should create a new MapK whose values correspond to the original map values, transformed by f") {
155-
val ezioList = ezio.mapValues(Lambda[cats.Id ~> List](v => List(v, v)))
183+
val ezioList = ezio.mapValues(new FunctionK[cats.Id, List] {
184+
def apply[A](a: A) = List(a, a)
185+
})
156186
ezioList(Name) shouldEqual List("Ezio Auditore", "Ezio Auditore")
157187
ezioList(Age) shouldEqual List(65, 65)
158188
ezioList(Fingers) shouldEqual List(9, 9)
@@ -161,9 +191,11 @@ class MapKSpec extends AnyFunSpec with Matchers {
161191

162192
describe(".map(f)") {
163193
it ("should create a new MapK whose entries correspond to the original map entries, transformed by f") {
164-
val restoreFinger = Lambda[ezio.Entry ~> cats.Id] {
165-
case (Fingers, n) => n + 1
166-
case (k, v) => v
194+
val restoreFinger = new FunctionK[ezio.Entry, cats.Id] {
195+
def apply[A](entry: ezio.Entry[A]) = entry match {
196+
case (Fingers, n) => n + 1
197+
case (k, v) => v
198+
}
167199
}
168200
val ezio2 = ezio.map(restoreFinger)
169201
ezio(Fingers) shouldEqual 9
@@ -191,8 +223,8 @@ class MapKSpec extends AnyFunSpec with Matchers {
191223
val max = new FunctionK[MyKey, MapK[MyKey, cats.Id]#Combiner] {
192224
def apply[A](fa: MyKey[A]) = fa match {
193225
case Name => (l: String, r: String) => (if (l.compareToIgnoreCase(r) > 0) l else r): String
194-
case Age => _ max _
195-
case Fingers => _ max _
226+
case Age => (l: Int, r: Int) => l max r
227+
case Fingers => (l: Int, r: Int) => l max r
196228
}
197229
}
198230
val merged1 = ezio.merge(dylan, max)

src/test/scala/com/codedx/util/MutableMapKSpec.scala

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,22 @@ class MutableMapKSpec extends AnyFunSpec with Matchers{
3434
}
3535

3636
describe("MutableMapK") {
37+
describe("equals and hashCode") {
38+
it ("should behave properly for identical MutableMapK instances") {
39+
val a = makeDemoMap
40+
val b = makeDemoMap
41+
a.equals(b) shouldBe true
42+
a.hashCode shouldEqual b.hashCode
43+
}
44+
it ("should behave properly for non-identical MutableMapK instances") {
45+
val a = makeDemoMap
46+
val b = makeDemoMap
47+
b.add(Age, 20)
48+
a.equals(b) shouldBe false
49+
a.hashCode should not equal b.hashCode
50+
}
51+
}
52+
3753
describe(".contains(key)") {
3854
it ("should return whether or not the map contains the given key") {
3955
makeDemoMap.contains(Extra) shouldBe false
@@ -112,7 +128,9 @@ class MutableMapKSpec extends AnyFunSpec with Matchers{
112128

113129
describe(".mapValues(f)") {
114130
it ("should create a new MutableMapK whose values correspond to the original map values, transformed by f") {
115-
val mapped: MutableMapK[MyKey, List] = makeDemoMap.mapValues(Lambda[cats.Id ~> List](v => List(v, v)))
131+
val mapped: MutableMapK[MyKey, List] = makeDemoMap.mapValues(new FunctionK[cats.Id, List] {
132+
def apply[A](v: A) = List(v, v)
133+
})
116134
mapped(Name) shouldEqual List("demo", "demo")
117135
mapped(Age) shouldEqual List(10, 10)
118136
}
@@ -154,7 +172,7 @@ class MutableMapKSpec extends AnyFunSpec with Matchers{
154172
val seenValuesB = List.newBuilder[Any]
155173
val seenKeysB = Set.newBuilder[MyKey[_]]
156174
val m = makeDemoMap
157-
m.foreach(new FunctionK[m.Entry, Lambda[x => Unit]] {
175+
m.foreach(new FunctionK[m.Entry, ({ type F[x] = Unit })#F] {
158176
def apply[A](fa: (MyKey[A], Id[A])) = {
159177
seenKeysB += fa._1
160178
seenValuesB += fa._2

0 commit comments

Comments
 (0)