Reputation: 1305
I have a class of objects which either have one type of ID or another:
sealed trait ItemId
case class NumericId(id: Int) extends ItemId
case class StringId(id: String) extends ItemId
sealed trait Item {
def id: ItemId
}
case class ItemWithNumericId(id: NumericId) extends Item
case class ItemWithStringId(id: StringId) extends Item
I'd like to create an interface for some sort of service which retrieves items:
trait ItemService[IdType <: ItemId, ItemType <: Item] {
def get(id: IdType): ItemType
}
How do I link the ItemId
type with the Item
type to put a constraint on ItemService
s to not allow something like:
class SillyItemService extends ItemService[NumericId, ItemWithStringId] {
def get(id: NumericId): ItemWithStringId = ???
}
I've realised I can add generic types to the Item
class:
sealed trait ItemId
case class NumericId(id: Int) extends ItemId
case class StringId(id: String) extends ItemId
sealed trait Item[Id <: ItemId] {
def id: Id
}
case class ItemWithNumericId(id: NumericId) extends Item[NumericId]
case class ItemWithStringId(id: StringId) extends Item[StringId]
trait ItemService[IdType <: ItemId, ItemType <: Item[IdType]] {
def get(id: IdType): ItemType
}
which is OK but It's super verbose. Ideally, the service would only have a single generic type.
Thanks very much for any answers/input.
Upvotes: 2
Views: 165
Reputation: 18424
This would be my approach:
sealed trait ItemId
case class NumericId(id: Int) extends ItemId
case class StringId(id: String) extends ItemId
trait Item[A <: ItemId] {
def id: A
}
trait ItemService[A <: ItemId, B <: Item[A]] {
def get(id: A): B
}
It's honestly not much different from what you had done, I just think there's not much need to make the Item
trait sealed and introduce two implementations right there.
If you have no use for narrowing the return type of the get
method of a specific ItemService
, you could even leave off the B
type parameter to make things one step simpler:
trait ItemService[A <: ItemId] {
def get(id: A): Item[A]
}
Upvotes: 3
Reputation: 28511
Path dependant types are one option which is covered by the other answer, but for the purposes of flexibility I would personally go with implicits/context bounds in this case.
trait Proof[IdType <: ItemId, ItemType <: Item[IdType]]
trait ItemService[IdType <: ItemId, ItemType <: Item[IdType]] {
def get(id: IdType)(implicit ev: Proof[IdType, ItemType])
}
You can also make trait
an abstract class
to hoist the declaration of the implicit proof, or whatever other party trick from the many available lets you sidestep the need to include the evidence on every method on the service.
Then I'd make a companion object for Proof
and list the correlations feasible within your domain.
object Proof {
implicit numericProof: Proof[NumericId, ItemWithNumericId] = new Proof[NumericId, ItemWithNumericId] {}
...
}
At this point you don't really care what your service looks like, though f bounded polymorphism may later allow you for super fine grained control, as you can specialise implicits for specific implementations as well as create ambiguous proofs where you want to give compile time errors for things that aren't meant to be mixed together.
Upvotes: 2
Reputation: 40500
Something like this maybe?
trait Item {
type IdType
def id: IdType
}
trait ItemService[I <: Item] {
def get(id: I#IdType): Item
}
Upvotes: 2