J Cracknell
J Cracknell

Reputation: 3547

Type-parametrized type hierarchies

I was wondering if anyone has any experience with creating a type-parametrized type hierarchy? I am fairly certain this is acheivable as a result of scala's pseudo-unification of packages & static objects.

The specific use case I have in mind is parametrizing an id type over an application framework so you can use your choice of int/long/java.util.UUID/BSONId/whatever. Consider as a rough example:

package foolib.generic

trait GenEntity[I] { def id: I }

trait GenRepository[I] { def getById(id: I): GenEntity[I] }

trait FooTaxonomy[I] {
  type Entity = GenEntity[I]
  type Repository = GenRepository[I]

  object subpackage extends generic.subpackage.SubpackageTaxonomy[I]
}

You would then configure the hierarchy for use in a project with something like:

package object myprj {
  object foolib extends foolib.generic.FooTaxonomy[java.util.UUID]

  // Whee!
  val someEntity = new myprj.foolib.Entity(java.util.UUID.randomUUID())
}

Are there any reasons this is a spectacularly bad idea? Any pitfalls/etc I should be aware of?

Upvotes: 1

Views: 111

Answers (1)

Marius Danila
Marius Danila

Reputation: 10411

This approach would work but you may encounter problems when the number of type parameters increases. Perhaps a solution would be to use abstract type members instead of type parameters.

Another approach is to use the cake pattern which I think provides a better solution in your case. The exact logic of your code eludes me a bit, so this rewrite may not entirely represent your intention:

package foolib.generic


//defines common types used by all modules
trait CoreModule {

    type Id // abstract type, not commited to any particular implementation

}

//module defining the EntityModule trait
trait EntityModule { this: CoreModule => //specifying core module as a dependency

    trait GenEntity {
        def id: Id
    }

    def mkEntity(id: Id): Entity //abstract way of creating an entity

}

//defines the GenRepository trait
trait RepositoryModule { this: EntityModule with CoreModule => //multiple dependencies

    trait GenRepository {
        def getById(id: Id): GenEntity
    }

    val repository: GenRepository //abstract way of obtaining a repository

}

//concrete implementation for entity
trait EntityImplModule extends EntityModule { this: CoreModule =>
    case class Entity(val id: Id) extends GenEntity

    def mkEntity(id: Id) = Entity(id)
}

//modules that provides a concrete implementation for GenRepository
trait RepositoryImplModule extends RepositoryModule { this: CoreModule with EntityModule =>

    object RepositoryImpl extends GenRepository {
        def getById(id: Id) = mkEntity(id)
    }

}

//this unifies all your modules. You can also bind any dependencies and specify any types
object Universe
    extends CoreModule
    with EntityImplModule
    with RepositoryImplModule {

    type Id = java.util.UUID

    val repository = RepositoryImpl

    def mkEntity(id: Id) = Entity(id)

}

//usage
object Main {

    import Universe._
    import java.util.UUID

    val entity = repository.getById(UUID.randomUUID())
    println(entity.id)

}

This achieves your goal of creating an implementation independent of the concrete type Id and it also provides a nice way to do dependency injection.

The modules which provide a concrete implementation for GenRepository, for instance, may require a concrete type for Id. You can very well create another module which binds Id to a concrete type and make the RepositoryImplModule depend on the former module, thus specifying that this concrete implementation of GenRepository will work only for a certain type of ids.

The Cake pattern is very powerful and has many variations. This video explains it quite well, I recommend you watch it if you are interested in this solution:

Cake Pattern: The Bakery from the Black Lagoon

Upvotes: 1

Related Questions