William Carter
William Carter

Reputation: 1305

Linking Types in Scala

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 ItemServices 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

Answers (3)

Joe K
Joe K

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

flavian
flavian

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

Dima
Dima

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

Related Questions