Simão Martins
Simão Martins

Reputation: 1240

Compile time guarantee a map has a key for each enum case

Given the following enum:

enum Connector:
  case CHAdeMO
  case Mennekes
  case CCS
  case Tesla

Is there a type like Map[ConnectorType, Int] but which would produce a compile error for:

Map(
  Connector.CHAdeMO -> 1,
  Connector.Mennekes -> 2,
  Connector.CCS -> 3,
)

That is the map does not contain a key for Connector.Tesla. In other words, in compile time the type should be akin to ((Connector.CHAdeMO, Int), (Connector.Mennekes, Int), (Connector.CCS, Int), (Connector.Tesla, Int)) but behave like a regular Map otherwise.

Upvotes: 6

Views: 167

Answers (1)

user
user

Reputation: 7604

Here's a solution using tuples (Scastie):

opaque type CheckedMap <: Map[Connector, Int] = Map[Connector, Int]

type Contains[E, T <: Tuple] <: Boolean = T match {
  case EmptyTuple => false
  case h *: t =>
    h match {
      case (E, _) => true
      case _      => Contains[E, t]
    }
}

type ContainsAll[S <: Tuple, T <: Tuple] = S match {
  case EmptyTuple => DummyImplicit
  case h *: t =>
    Contains[h, T] match {
      case true  => ContainsAll[t, T]
      case false => Nothing
    }
}

type AllConnectors = (
    Connector.CHAdeMO.type,
    Connector.Mennekes.type,
    Connector.CCS.type,
    Connector.Tesla.type
)

def checkedMap[T <: Tuple](t: T)(using
    @annotation.implicitNotFound(
      "Not all Connector types given."
    ) c: ContainsAll[AllConnectors, T]
): CheckedMap = t.toList.asInstanceOf[List[(Connector, Int)]].toMap

This will take a tuple of tuples (Connector, Int) and check if it contains all the types in another tuple of all Connector types. If the input contains all the connector types at least once, it looks for an implicit DummyImplicit, otherwise, it looks for an implicit Nothing, which it obviously doesn't find. The resulting error message is quite lengthy and unhelpful, so I put in a custom error message. Note that this doesn't check if there are duplicate keys, but could be trivially modified to do so.

Unfortunately, I found myself having to explicitly annotate the key-value pairs' types at the use site:

//Errors because Tesla is missing
checkedMap(
  (
    Connector.CHAdeMO -> 1: (Connector.CHAdeMO.type, Int),
    Connector.Mennekes -> 2: (Connector.Mennekes.type, Int),
    Connector.CCS -> 3: (Connector.CCS.type, Int)
  )
)
//Valid
checkedMap(
  (
    Connector.CHAdeMO -> 1: (Connector.CHAdeMO.type, Int),
    Connector.Mennekes -> 2: (Connector.Mennekes.type, Int),
    Connector.CCS -> 3: (Connector.CCS.type, Int),
    Connector.Tesla -> 4: (Connector.Tesla.type, Int)
  )
)

Upvotes: 5

Related Questions