Search code examples
scalatraitsdependent-typepath-dependent-typetype-members

How to make it so that dependent types in two different traits are recognized as the same type


I'm running into an issue where I am working with several Traits that use dependent typing, but when I try to combine the Traits in my business logic, I get a compilation error.

import java.util.UUID

object TestDependentTypes extends App{
  val myConf = RealConf(UUID.randomUUID(), RealSettings(RealData(5, 25.0)))

  RealConfLoader(7).extractData(myConf.settings)

}

trait Data

case class RealData(anInt: Int, aDouble: Double) extends Data

trait MySettings

case class RealSettings(data: RealData) extends MySettings

trait Conf {
  type T <: MySettings
  def id: UUID
  def settings: T
}

case class RealConf(id: UUID, settings: RealSettings) extends Conf {
  type T = RealSettings
}

trait ConfLoader{
  type T <: MySettings
  type U <: Data
  def extractData(settings: T): U
}

case class RealConfLoader(someInfo: Int) extends ConfLoader {
  type T = RealSettings
  type U = RealData
  override def extractData(settings: RealSettings): RealData = settings.data
}

The code in processor will not compile because extractData expects input of type ConfLoader.T, but conf.settings is of type Conf.T. Those are different types.

However, I have specified that both must be subclasses of MySettings, so it should be the case I can use one where the other is desired. I understand Scala does not compile the code, but is there some workaround so that I can pass conf.settings to confLoader.extractData?

===

I want to report that for the code I wrote above, there is a way to write it that would decrease my usage of dependent types. I noticed today while experimenting with Traits that Scala supports subclassing on defs and vals on classes that implement the Trait. So I only need to create a dependent type for the argument for extractData, and not the output.

import java.util.UUID

object TestDependentTypes extends App{
  val myConf = RealConf(UUID.randomUUID(), RealSettings(RealData(5, 25.0)))

  RealConfLoader(7).extractData(myConf.settings)

  def processor(confLoader: ConfLoader, conf: Conf) = confLoader.extractData(conf.settings.asInstanceOf[confLoader.T])
}

trait Data

case class RealData(anInt: Int, aDouble: Double) extends Data

trait MySettings

case class RealSettings(data: RealData) extends MySettings

trait Conf {
  def id: UUID
  def settings: MySettings
}

case class RealConf(id: UUID, settings: RealSettings) extends Conf

trait ConfLoader{
  type T <: MySettings
  def extractData(settings: T): Data
}

case class RealConfLoader(someInfo: Int) extends ConfLoader {
  type T = RealSettings
  override def extractData(settings: RealSettings): RealData = settings.data
}

The above code does the same thing and reduces dependence on dependent types. I have only removed processor from the code. For the implementation of processor, refer to any of the solutions below.


Solution

  • The code in processor will not compile because extractData expects input of type ConfLoader.T, but conf.settings is of type Conf.T. Those are different types.

    In the method processor you should specify that these types are the same.

    Use type refinements (1, 2) for that: either

    def processor[_T](confLoader: ConfLoader { type T = _T }, conf: Conf { type T = _T }) = 
      confLoader.extractData(conf.settings)
    

    or

    def processor(confLoader: ConfLoader)(conf: Conf { type T = confLoader.T }) = 
      confLoader.extractData(conf.settings)
    

    or

    def processor(conf: Conf)(confLoader: ConfLoader { type T = conf.T }) = 
      confLoader.extractData(conf.settings)