Mike Roberts
Mike Roberts

Reputation: 550

Possible to group types for passing to generic class?

I'm building a repository that I would like to have a consistent interface across multiple implementations. This repository needs three pieces so it know what to do: an Id, a Model and a Event. I define these as traits, then group them together as a Protocol.

trait AbstractId
trait AbstractModel
trait AbstractEvent
abstract class Protocol 

Here are a couple of Protocols:

object BusinessProtocol extends Protocol {
  final case class Id(id: Int) extends AbstractId
  final case class Model(id: Id, name: String) extends AbstractModel

  sealed abstract class Event extends AbstractEvent
  final case class Create(id: Id, name: String) extends Event
}

object Order {
  final case class Id(id: Uid) extends AbstractId
  final case class Model(id: Id, cost: BigDecimal) extends AbstractModel

  sealed abstract class Event extends AbstractEvent
  final case class Create(id: Id, cost: BigDecimal) extends Event
  final case class Close(id: Id) extends Event
}

Now I define my Repository interface. It has to take in a Model, Event and Id separately.

trait Repository[M <: AbstractModel, E <: AbstractEvent, I <: AbstractId] {
  def hydrate(id: I): M
  def persist(id: I, event: E)
}

And for completness, here's what a implemented Repository looks like:

object BusinessRepo extends Repository[BusinessProtocol.Model, BusinessProtocol.Event, BusinessProtocol.Id] {
  override def hydrate(id: Id): Model = ???

  override def persist(id: Id, event: BusinessProtocol.Event): Unit = ???
}

This all works, but I'd like to have a way to force two things:

1) Make Protocols provide definitions of Id, Model and Event.

2) Make the Repository just take a Protocol, and be able to pull the Id, Model and Event from the Protocol. Something like:

trait Repository[P <: Protocol] {
  def hydrate(id: P.Id): P.Model
  def persist(id: P.Id, event: P.Event)
}

This way, I can force Repository implementations to be always work on the three types that related. Is this possible?

Upvotes: 2

Views: 98

Answers (1)

Chirlo
Chirlo

Reputation: 6132

Let's try this with some type-level programming:

 trait Protocol {
   type E <: AbstractEvent    // define and Event  that extends AbstractEvent
   type I <: AbstractId          //....
   type M <: AbstractModel
} 

An implementation from this would be:

 object  BusinessProtocol {
   final case class Id(id: Int) extends AbstractId
   final case class Model(id: Id, name: String) extends AbstractModel
   sealed abstract class Event extends AbstractEvent
   final case class Create(id: Id, name: String) extends Event
 }

 class  BusinessProtocol extends Protocol{  
   import BusinessProtocol._
   //here they're assigned to the type variables
   type E = Event
   type I = Id
   type M = Model
}

And then the Repo:

trait Repository { 
   type P <: Protocol
   def hydrate(id: P#I): P#M
   def persist(id: P#I, event : P#E)
}

A BusinessRepo would then be:

 class BusinessRepo extends Repository{
    type P = BusinessProtocol

    import BusinesProtocol._

    def hydrate(id: Id) : Model = {...}
    def persist(id:Id, event: Event) = {...}
}

You can find the science behind this on this question

EDIT : I experimented a little bit more, and you could do the Repo like this too:

 trait Repository[P <: Protocol]{
     def hydrate(id: P#I): P#M
     def persist(id: P#I, event : P#E)
 } 

 class BusinessRepo extends Repository[BusinessProtocol]{
   ....
 }

Same effect, but his one makes it the signature of Repository more explicit.

Upvotes: 3

Related Questions