ClassTag and path-dependent types in a cake-pattern-like flavour

143 Views Asked by At

I am working on a slick project and I am trying to make my database layer easily swappable between different profiles in order to write tests on an in-memory database. This question is inspired by this problem but it doesn't have anything to do with slick itself.

I don't have a great deal of experience with dependent types, in my case I have the following trait that I use to abstract away some types from the database:

trait Types {
  type A <: SomeType
  type B <: SomeOtherType
  val bTag: ClassTag[B]
}

Then I have another trait which is basically a slice of my (faux) cake pattern:

trait BaseComponent {
  type ComponentTypes <: Types

  val a: Types#A
  implicit val bTag: ClassTag[Types#B]
}

Then I have an actual implementation of my component that can be seen as follows:

trait DefaultTypes {
  type A = SomeConcreteType
  type B = SomeOtherConcreteType
  val bTag = implicitly[ClassTag[B]]
}

trait DefaultBaseComponent extends BaseComponent {
  type ComponentTypes = DefaultTypes
  val ct = new ComponentTypes {}

  implicit val bTag = ct.bTag
}

I need the tag because later on a service will need it (in my actual implementation I use this type to abstract over different type of exceptions thrown by different DB libraries); I am quite sure that there is a much better way to do what I am trying to do.

If I do not instantiate the ComponentTypes trait in order to get the tag and I move the implicit-conjuring code in the DefaultBaseComponent it will conjure a null in place of the ClassTag. I need to have a way to refer to the actual types that I am using (the different A and B that I have in my different environments) and I need to do it in other components without knowing which actual types they are.

My solution works, compiles and pass all the tests I wrote for it, can anyone help me in getting it better?

Thank you!

1

There are 1 best solutions below

0
On

Your example is a bit unclear with all these Defaults and Components - maybe a more concrete example (e.g. DatabaseService / MysqlDatabaseService) would make it clearer?

You need to pass the ClassTag around wherever it's abstract - you can only "summon" one when you have a concrete type. You might like to package up the notion of a value and its tag:

trait TaggedValue[A] {val a: A; val ct: ClassTag[A]}
object TaggedValue {
  def apply[A: ClassTag](a1: A) =
    new TaggedValue[A] {
      val a = a1
      val ct = implicitly[ClassTag[A]]
    }
}

but this is just a convenience thing. You could also turn some of your traits into abstract classes, allowing you to use [A: ClassTag] to pass the tags implicitly, but obviously this affects which classes you can multiply inherit.

If you're hitting nulls that sounds like a trait initialization order problem, though without a more specific error message it's hard to help. You might be able to resolve it by replacing some of your vals with defs, or by using early initializers.