Reputation: 113
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?
Upvotes: 1
Views: 214
Reputation: 6172
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
// ...
}
}
Upvotes: 3
Reputation: 28511
I can see a few major improvements. Let's start from the beginning, first of all we never use val
inside a trait for abstract members, look here.
trait Request[Payload] {
def metadata: String // Not relevant
def payload: Payload
}
Now let's look here:
object Request {
def apply[P, R <: Request[P]](code: String)(metadata: String, payload: P): R = code match {
case "S" => RequestWithString(metadata, payload)
case "I" => RequestWithInt(metadata, payload)
}
}
You are misunderstanding the meaning of P <: Request[P]
, this is an f-bounded type polymorphic param, which is used for what's known as type refinement, e.g return the most specific wrapper type after invoking a method defined on the upper type bound, e.g have methods on Request
return RequestWithInt
instead of just simply Request
. In your case I don't think you are picking the right approach anyhow.
You would use it for a method that takes in both RequestWithString
and RequestWithInt
instances as params or something like that.
Now in your case what you should do is to use an ADT for your request type. Something like RequestEncoder
.
trait RequestEncoder[T] {
def encode(obj: T): String
def decode(obj: String): T
}
object RequestEncoder {
implicit val intEncoder = new RequestEncoder[Int] {
def encode(obj: Int): String = obj.toString
def decode(source: String): Int = source.toInt
}
}
trait Request[Payload : RequestEncoder] {
def metadata: String // Not relevant
def payload(source: Payload): String = implicitly[RequestEncoder[Payload]].encode(source)
}
Upvotes: 2