0
\$\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.stack.imgur.com/ZT7o0.jpg

\$\endgroup\$
2
\$\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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.