2

Suppose I'm attempting to model entities, metadata, and repositories using inheritance

trait EntityMetadata {
  def maybeVersion: Option[Int]
}

// For creation
case object NoMetadata extends EntityMetadata {
  def maybeVersion: Option[Int] = None
}

// For other cases
final case class VersionedMetadata(version: Int) extends EntityMetadata {

  def maybeVersion: Option[Int] = Some(version)
}

trait Entity[Meta <: EntityMetadata] {
  type Id
  def id: Id
  def meta: Meta // Meta is paremeterised
}

If I then try to create a trait to hold some methods for a general purpose backing store, it seems to me like even though the types are known...I can't actually use them properly?

trait BackingStore {

  // Method for retrieving an entity by id
  // `Meta` doesn't really matter here, but we can't
  // wild-card it. We return the Entity with VersionedMetadata
  // since it's been stored if we can find it
  def getFromStore[Meta <: EntityMetadata, E[_] <: Entity[_]](
    id: E[Meta]#Id
  ): Option[E[VersionedMetadata]]

  // Just for demo purposes, try to retrieve something by id
  // and return its metadata version
  def getVersion[Meta <: EntityMetadata, E[_] <: Entity[_]](
    id: E[Meta]#Id
  ): Option[Long] = getFromStore(id).map { retrieved =>
    // So far so good, we know it's E[VersionedMetadata]
    val typeTest: E[VersionedMetadata] = retrieved

    //
    // value version is not a member of _$2
    // typeTest.meta.version // complains about version
    //
    retrieved.meta.version // complains about version

  }

}

I'm trying to work out:

  1. Why the compiler thinks that retrieved.meta doesn't have .version, or, in fact, anything really beyond what Any/Object has.
  2. What I can do to make this work
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
lloydmeta
  • 1,289
  • 1
  • 15
  • 25

1 Answers1

2

Try to fix signatures

def getFromStore[Meta <: EntityMetadata, E[M <: EntityMetadata] <: Entity[M]](
  id: E[Meta]#Id
): Option[E[VersionedMetadata]]

def getVersion[Meta <: EntityMetadata, E[M <: EntityMetadata] <: Entity[M]](
  id: E[Meta]#Id
): Option[Long]

E[_] and Entity[_] in E[_] <: Entity[_] are different: E[_] is a type constructor (i.e. you can have a type E[M] for every type M), Entity[_] aka Entity[Meta] forSome { type Meta } is an existential type. Existential type doesn't have .version (retrieved.meta was of type Any).


Another way to fix your code is

def getFromStore[Meta <: EntityMetadata, E[_] <: Entity[_]](
  id: E[Meta]#Id
): Option[E[VersionedMetadata]]

def getVersion[Meta <: EntityMetadata, E[_] <: Entity[_ <: EntityMetadata]](
  id: E[Meta]#Id
): Option[Int] = getFromStore(id).flatMap { retrieved =>
  val typeTest: E[VersionedMetadata] = retrieved

  retrieved.meta.maybeVersion
}

I kept type constructor and existential type but added upper bound <: EntityMetadata to parameter of existential type Entity[_ <: ...], which is upper bound for type parameter E[_] <: .... Now retrieved.meta has type that is a subtype of EntityMetadata so it has .maybeVersion instead of .version (and map should be replaced with flatMap). Also Long should be replaced with Int.

Or you could put upper bound _ <: VersionedMetadata instead of my <: EntityMetadata. Then you could keep .version, .map and Long.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • Thanks ! The first solution worked beautifully in my case. Really liked your type constructor explanation as well; in hindsight it makes perfect sense :) – lloydmeta Apr 20 '19 at 12:40