Home > Net >  How do I limit the parameter types of a generic argument based on another parameter in Scala?
How do I limit the parameter types of a generic argument based on another parameter in Scala?

Time:02-06

I'm trying to create a map of different configurations, where each configuration has a given key object and some options, e.g.

  • FirstConfig can be either:
    • FirstConfigOptionA
    • FirstConfigOptionB
  • SecondConfig can be either:
    • SecondConfigOptionA
    • SecondConfigOptionB
  • ...

And I'm having trouble with general typing and signature of the setter function so it checks at compile time I'm supplying the correct objects, e.g.

// 1. this should compile normally
set(FirstConfig, FirstConfigOptionA)

// 2. should NOT compile due to `SecondConfigOptionA` parameter not being a valid option for `FirstConfig`
set(FirstConfig, SecondConfigOptionA)

So far, my attempts still allow the second case above to compile.

abstract sealed class Configuration
trait OptionKey[T <: Configuration] {}
trait OptionVariant[T <: Configuration] {}

// First Config
trait FirstConfig extends Configuration
object FirstConfigKey extends OptionKey[FirstConfig];
object FirstConfigOptionA extends OptionVariant[FirstConfig]
object FirstConfigOptionB extends OptionVariant[FirstConfig]

// Second Config
trait SecondConfig extends Configuration
object SecondConfigKey extends OptionKey[SecondConfig];
object SecondConfigOptionA extends OptionVariant[SecondConfig]
object SecondConfigOptionB extends OptionVariant[SecondConfig]

def set[T](k: OptionKey[T], v: OptionVariant[T]): Unit = {}

set(FirstConfigKey, FirstConfigOptionA)
set(FirstConfigKey, SecondConfigOptionA) // This still compiles

I've also tried using Enumerations with similar results:

object FirstConfig extends Enumeration {
  type FirstConfig = Value
  val FirstConfigOptionA, FirstConfigOptionB = Value
}

object SecondConfig extends Enumeration {
  type SecondConfig = Value
  val SecondConfigOptionA, SecondConfigOptionB = Value
}

def set(k: Enumeration, v: Enumeration#Value): Unit = {}

set(FirstConfig, FirstConfig.FirstConfigOptionA)
set(FirstConfig, SecondConfig.SecondConfigOptionA) // This still compiles

What is the correct way to express this relationship between a config and its available options or what should be set's signature to enforce it?

CodePudding user response:

What about using path-dependant types like this:

trait Configuration {
  sealed trait OptionKey
  val key: OptionKey
  
  sealed trait OptionVariant
}

object Configuration {
  def set(config: Configuration)(variant: config.OptionVariant): Unit = {
    println(s"${config} - ${config.key} - ${variant}")
  }
}

case object FirstConfig extends Configuration {
  private case object FirstConfigKey extends OptionKey
  override final val key: OptionKey = FirstConfigKey
  
  case object FirstConfigOptionA extends OptionVariant
  case object FirstConfigOptionB extends OptionVariant
}

case object SecondConfig extends Configuration {
  private case object SecondConfigKey extends OptionKey
  override final val key: OptionKey = SecondConfigKey
  
  case object SecondConfigOptionA extends OptionVariant
  case object SecondConfigOptionB extends OptionVariant
}

You can see it working as expected here.

CodePudding user response:

Why do you need to store them as key/value pairs? You can just represent it as an algebraic data type:

enum FirstConfig:
  case OptionA
  case OptionB

enum SecondConfig:
  case OptionA
  case OptionB

enum Config:
  case First(value: FirstConfig)
  case Second(value: SecondConfig)

def set(config: Config): Unit = …
  •  Tags:  
  • Related