scalagenericsscala-3

How to overcome Scala 3 missing type projection?


In Scala 2 I used type projection in an simple ORM model which contains of IdEntity and some Repositories. Now I tried to migrate to Scala 3 but stuck since type projection with # is missing.

Simplified code looks like:

trait IdEntity:
  type ID_Type
  val id: ID_Type

trait IdLongEntity extends IdEntity:
  override type ID_Type = Long

trait IdStringEntity extends IdEntity:
  override type ID_Type = String

trait IdRepository[E <: IdEnity]:
  def findById(id: E#ID_Type): Option[E]:

Any ideas how to overcome the missing type projection in def findById(id: E#ID_Type)?

I already tried with type match like

object IdEntity:
  type Aux[K] = IdEntity {type ID_Type = K}

type IdType[T <: IdEntity] = T match
    case IdEntity.Aux[k] => k

but this does not work since the compile thinks that the types does not match.

Update:

Thanks to the answer I tried type classes as well as match type but both fail as soon as I try the following (https://scastie.scala-lang.org/UtW9u0mbS5ydiZmQNNWV6g):

trait IdEntity:
  type ID_Type
  val idDefault: ID_Type
  val id: Option[ID_Type]
  def idOrDefault: ID_Type = id.getOrElse(idDefault)

trait IdRepository[E <: IdEntity](using val e: E):
  type ID_Type = e.ID_Type  
  def delete(id: ID_Type): Either[String, Int] = deleteImpl(id)
  def delete(entity: E): Either[String, Int] = delete(entity.idOrDefault) // here the type error occures
  protected def deleteImpl(id: ID_Type): Either[String, Int]

Solution

  • trait IdEntity:
      type ID_Type
      val id: ID_Type
    object IdEntity:
      type Aux[K] = IdEntity {type ID_Type = K}
    
    trait IdLongEntity extends IdEntity:
      override type ID_Type = Long
    
    trait IdStringEntity extends IdEntity:
      override type ID_Type = String
    
    // match type
    type IdType[T <: IdEntity] = T match
      case IdEntity.Aux[k] => k
    
    trait IdRepository[E <: IdEntity]:
      def findById(id: IdType[E]): Option[E] = ???
    

    https://scastie.scala-lang.org/QoV6Z7xRTOa6hVhAYOb0uw

    this does not work since the compile thinks that the types does not match

    You should prepare reproduction and we'll see whether compilation can be fixed.

    trait IdEntity:
      type ID_Type
      val id: ID_Type
    object IdEntity:
      type Aux[K] = IdEntity {type ID_Type = K}
    
    trait IdLongEntity extends IdEntity:
      override type ID_Type = Long
    
    trait IdStringEntity extends IdEntity:
      override type ID_Type = String
    
    // type class
    trait IdType[T <: IdEntity]:
      type Out
    object IdType:
      given [K, T <: IdEntity.Aux[K]]: IdType[T] with
        type Out = K
    
    trait IdRepository[E <: IdEntity]:
      def findById(using idType: IdType[E])(id: idType.Out): Option[E] = ???
    

    https://scastie.scala-lang.org/166gIUf3Rg2DwSgLlheXvQ

    Or you can even try to use type classes starting from IdEntity, for example making IdEntity a type class:

    trait IdEntity:
      type ID_Type
      val id: ID_Type
    
    trait IdLongEntity extends IdEntity:
      override type ID_Type = Long
    
    trait IdStringEntity extends IdEntity:
      override type ID_Type = String
    
    trait IdRepository[E <: IdEntity](using val e: E):
      def findById(id: e.ID_Type): Option[E]
    

    https://scastie.scala-lang.org/ap4GJY9NQyOyt2eHwobXSg


    In Scala 3, how to replace General Type Projection that has been dropped?

    What does Dotty offer to replace type projections?

    E is not a legal path since it is not a concrete type (Scala 3)

    https://docs.scala-lang.org/scala3/reference/dropped-features/type-projection.html


    Translation of

    // scala 2.13, type projections
    
    trait IdEntity {
      type ID_Type
      val idDefault: ID_Type
      val id: Option[ID_Type]
      def idOrDefault: ID_Type = id.getOrElse(idDefault)
    }
    
    trait IdRepository[E <: IdEntity] {
      type ID_Type = E#ID_Type
      def delete(id: ID_Type): Either[String, Int] = deleteImpl(id)
      def delete(entity: E): Either[String, Int] = delete(entity.idOrDefault)
      protected def deleteImpl(id: ID_Type): Either[String, Int]
    }
    

    can be

    // scala 3, match types
    
    trait IdEntity:
      type ID_Type
      val idDefault: ID_Type
      val id: Option[ID_Type]
      def idOrDefault: ID_Type = id.getOrElse(idDefault)
    object IdEntity:
      type Aux[K] = IdEntity {type ID_Type = K}
    
    type IdType[T <: IdEntity] = T match
      case IdEntity.Aux[k] => k
    
    // unfortunately, E <: IdEntity.Aux[IdType[E]] doesn't work (match types are not lazy enough): Recursion limit exceeded. Maybe there is an illegal cyclic reference?
    trait IdRepository[E <: IdEntity]:
      type ID_Type = IdType[E]
      def delete(id: ID_Type): Either[String, Int] = deleteImpl(id)
      def delete(entity: E {type ID_Type = IdType[E]}): Either[String, Int] = delete(entity.idOrDefault)
      protected def deleteImpl(id: ID_Type): Either[String, Int]
    

    or

    // scala 3, type classes (path-dependent types)
    
    trait IdEntity:
      type ID_Type
      val idDefault: ID_Type
      val id: Option[ID_Type]
      def idOrDefault: ID_Type = id.getOrElse(idDefault)
    
    trait IdRepository[E <: IdEntity](using val e: E):
      type ID_Type = e.ID_Type
      def delete(id: ID_Type): Either[String, Int] = deleteImpl(id)
      def delete(entity: E {type ID_Type = e.ID_Type}): Either[String, Int] = delete(entity.idOrDefault)
      protected def deleteImpl(id: ID_Type): Either[String, Int]