1
\$\begingroup\$

I was working with some complicated generics system, and I found the need to make an abstraction for a typesafe mapping between K[T] and V[T] where T is the same for individual pairs.

In the case where the key or the value is T itself, we use a trick: type Identity[T] = T, and set the parameter to Identity. I have provided type aliases to make this usage more clear.

I would appreciate critiques on how I can make my code more robust, elegant, efficient, or idiomatic

/**
 * A map where the keys and values accept a type parameter, with a bound, and key-value pairs in the map have the same
 * type parameter.
 */
class TypeMatchingMap[K[_ <: B], V[_ <: B], B](private val contents: Map[Any, Any]) {
 def +[T <: B](kv: (K[T], V[T])): TypeMatchingMap[K, V, B] =
 new TypeMatchingMap[K, V, B](contents + kv)
 def -(k: K[_]): TypeMatchingMap[K, V, B] =
 new TypeMatchingMap[K, V, B](contents - k)
 def apply[T <: B](k: K[T]): V[T] =
 contents(k).asInstanceOf[V[T]]
 def get[T <: B](k: K[T]): Option[V[T]] =
 contents.get(k).map(_.asInstanceOf[V[T]])
 type Pair[T <: B] = (K[T], V[T])
 def toSeq: Seq[Pair[_]] =
 contents.toSeq.map(_.asInstanceOf[Pair[_]])
 override def toString: String = contents.toString
 override def equals(obj: scala.Any): Boolean =
 if (obj.isInstanceOf[TypeMatchingMap[Identity, Identity, _]])
 obj.asInstanceOf[TypeMatchingMap[Identity, Identity, Any]].contents == this.contents
 else false
 override def hashCode(): Int = contents.hashCode()
}
object TypeMatchingMap {
 type Identity[T] = T
 type Identified[K[_ <: B], B] = TypeMatchingMap[K, Identity, B]
 type Identifying[V[_ <: B], B] = TypeMatchingMap[Identity, V, B]
 private val _empty = new TypeMatchingMap[Identity, Identity, Any](Map.empty)
 def empty[K[_ <: B], V[_ <: B], B]: TypeMatchingMap[K, V, B] = _empty.asInstanceOf[TypeMatchingMap[K, V, B]]
}
object TypeMatchingMapTest extends App {
 sealed trait NumBox
 type BoxID[T <: NumBox] = UUID
 case class FloatBox(n: Float) extends NumBox
 case class DoubleBox(n: Double) extends NumBox
 case class IntBox(n: Int) extends NumBox
 var map: Identified[BoxID, NumBox] = TypeMatchingMap.empty[BoxID, Identity, NumBox]
 val id5f: BoxID[FloatBox] = UUID.randomUUID()
 map = map + (id5f -> FloatBox(5f))
 val id6d: BoxID[DoubleBox] = UUID.randomUUID()
 map = map + (id6d -> DoubleBox(6.0))
 val id7i: BoxID[IntBox] = UUID.randomUUID()
 map = map + (id7i -> IntBox(7))
 val f5: FloatBox = map(id5f)
 val d6: DoubleBox = map(id6d)
 val i7: IntBox = map(id7i)
 println(map)
 println(map.toSeq)
}

A use case? Say you have a map of entities, which are organized by UUIDs. The problem is that the type system does now acknowledge what kind of Entity an EntityID is pointing to. So, you can make a parametrized alias:

type EntityID[E <: Entity] = UUID

And store your entities in an Identified[EntityID, Entity]. Then, if you have, say an EntityID[Cow], you can pass it to the map and get your Cow.

Thank you, and for your troubles, a related image for your enjoyment:

https://i.sstatic.net/ZT7o0.jpg

asked Dec 8, 2017 at 2:19
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

It's been a couple of months so maybe you've moved on, but I had a look through this today. I also may have read too much into your example use case, so apologies if this isn't helpful.

If like in your example we can restrict the scope down to cases where K is covariant, and we can also remove the higher-kindedness of the value type, then we can improve the type safety significantly. So much so that we can escape all of the asInstanceOf calls!

class TypeMatchingMap[K[+_ <: B], B](private val contents: Map[K[B], B]) {
 def +[T <: B](kv: (K[T], T)): TypeMatchingMap[K, B] =
 new TypeMatchingMap[K, B](contents + kv)
 def -[T <: B](k: K[T]): TypeMatchingMap[K, B] =
 new TypeMatchingMap[K, B](contents - k)
 def apply[T <: B : ClassTag](k: K[T]): T =
 contents(k) match {
 case t: T => t
 case _ => throw ??? // Insert throwable here
 }
 def get[T <: B : ClassTag](k: K[T]): Option[T] =
 contents.get(k).flatMap {
 case t: T => Some(t)
 case _ => None
 }
 def toSeq: Seq[(K[B], B)] = contents.toSeq
 override def toString: String = contents.toString()
 override def equals(obj: Any): Boolean =
 obj match {
 case m: TypeMatchingMap[_, _] => m.contents == this.contents
 case _ => false
 }
 override def hashCode(): Int = contents.hashCode()
}
object TypeMatchingMap {
 def empty[K[+_ <: B], B]: TypeMatchingMap[K, B] =
 new TypeMatchingMap[K, B](Map.empty)
}
object TypeMatchingMapTest extends App {
 sealed trait NumBox
 type BoxID[+T <: NumBox] = UUID
 case class FloatBox(n: Float) extends NumBox
 case class DoubleBox(n: Double) extends NumBox
 case class IntBox(n: Int) extends NumBox
 var map: TypeMatchingMap[BoxID, NumBox] = TypeMatchingMap.empty[BoxID, NumBox]
 val id5f: BoxID[FloatBox] = UUID.randomUUID()
 map = map + (id5f -> FloatBox(5f))
 val id6d: BoxID[DoubleBox] = UUID.randomUUID()
 map = map + (id6d -> DoubleBox(6.0))
 val id7i: BoxID[IntBox] = UUID.randomUUID()
 map = map + (id7i -> IntBox(7))
 val f5: FloatBox = map(id5f)
 val d6: DoubleBox = map(id6d)
 val i7: IntBox = map(id7i)
 println(map)
 println(map.toSeq)
}

If you can't make the assumptions I've made above, I'd still recommend changing the signature of - to def -[T <: B](k: K[T]): TypeMatchingMap[K, V, B]. Consider the following:

val map: TypeMatchingMap[K, V, Int] = ???
val k: K[String] = ???
map - k

We know that the last line won't ever do anything useful, so in my mind it would be kinder to prevent it from compiling so as to prevent any client of the API from making a mistake.

Happy to discuss the code more, but didn't want to go into too much detail if you're no longer interested.

answered Feb 23, 2018 at 3:04
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.