I'd like to achieve some type safety in the following situation.
Basically, I have different types of requests that are stored in a database, their type being identified with some string code. For business reasons, this code does not match the class names.
Each type of request include some sort of payload, the type of the payload directly depends on the type of request.
Here is a simplified version of what I have achieved so far:
trait Request[Payload] {
def metadata: String // Not relevant
def payload: Payload
}
case class RequestWithString(override val metadata: String, override val payload: String) extends Request[String]
case class AnotherTypeOfRequestWithString(override val metadata: String, override val payload: String) extends Request[String]
case class RequestWithInt(override val metadata: String, override val payload: Int) extends Request[Int]
object Request {
def apply(code: String)(metadata: String, payload: Any): Request[_] = code match {
case "S" => RequestWithString(metadata, payload.asInstanceOf[String])
case "S2" => AnotherTypeOfRequestWithString(metadata, payload.asInstanceOf[String])
case "I" => RequestWithInt(metadata, payload.asInstanceOf[Int])
}
}
This is not satisfying as I would like Scala to infer the type of the payload to avoid casting, and the (parametered) type of the returned value.
What I am looking for is something like that:
object Request {
def apply[P, R <: Request[P]](code: String)(metadata: String, payload: P): R = code match {
case "S" => RequestWithString(metadata, payload)
case "S2" => AnotherTypeOfRequestWithString(metadata, payload)
case "I" => RequestWithInt(metadata, payload)
}
}
But this does not seem to work, I can't get rid of some type mismatch errors:
found : P
required: String
case "S" => RequestWithString(metadata, payload)
^
Shouldn't Scala infer that P is String in this case? What am I missing?
Move the matching decision logic to a typeclass:
// this typeclass holds the logic for creating a `Request` for
// a particular payload
sealed abstract class RequestPayloadType[T](val create: (String, T) => Request[T])
object RequestPayloadType {
implicit object StringPayloadType extends RequestPayloadType[String] (RequestWithString.apply)
implicit object IntPayloadType extends RequestPayloadType[Int] (RequestWithInt.apply)
}
object Request {
def apply[P:RequestPayloadType](metadata: String, payload: P): Request[P] =
implicitly[RequestPayloadType[P]].create(metadata, payload)
}
Common pattern in scala: Move the code that requires knowledge of certain types, to a compilation unit that has that knowledge.
Keep in mind, it might be cleaner to not have individual request classes, and just have a single parameterized one:
case class Request [P:RequestPayloadType](metadata: String, payload: P) {
// delegate any code that needs to know the type to `implicitly[RequestPayloadType[T]]...`
}
sealed trait RequestPayloadType[T] {
// specify here code that needs to know the actual type, i.e:
// def encode (value: T): String // abstract
// def decode (value: String): T // abstract
}
object RequestPayloadType {
implicit object StringPayloadType extends RequestPayloadType[String] {
// implement here any `String` specific code, .i.e:
// def encode (s: String) = s
// ...
}
implicit object IntPayloadType extends RequestPayloadType[Int] {
// implement here any `Int` specific code, .i.e:
// def encode (i: Int) = i.toString
// ...
}
}