scalatype-projection

What is the correct restriction on a type projection in Scala?


I have a difficulty with restrictions on type projections in Scala. Assume I have the following code:

sealed trait Color
case object Red extends Color
case object Green extends Color
case object Blue extends Color

trait Item {
  type Colors <: Color
}

object RedGreenItem extends Item {
  type Colors = Red.type with Green.type
}

object Test {
  def foo[I <: Item, C >: I#Colors <: Color](item : I, color : C) : Unit = {
    // ...
  }

  def main(args : Array[String]) : Unit = {
    foo(RedGreenItem, Blue) // <- why does it compile?
  }
}

What I actually want to achieve is to be able to pass tofoo as the second parameter only a color that is defined in Item#Colors (hence the restriction C >: I#Colors). But it turns out, I can pass every other color as well (Blue in this case).

I can see the source for the problem if I write the foo method like this:

def foo[I <: Item, C >: I#Colors <: Color : TypeTag](item : I, color : C) : Unit = {
  // ...
  println(s"type = ${typeOf[C]}")
}

// type = Color

So, instead of Blue.type the type is inferred as Color and therefore any color is okay. If I remove the restriction >: I#Colors, then the type is determined correctly:

def foo[I <: Item, C <: Color : TypeTag](item : I, color : C) : Unit = {
  // ...
  println(s"type = ${typeOf[C]}")
}

// type = Blue.type

So, my question is: How can I achieve my goal to have the restriction for the color to have an allowed type and not to compile otherwise? Many thanks in advance and sorry for maybe a stupid question.


Solution

  • Your type Colors is basically saying that you want your color to be both Red and Green. But what you want to express is that it has to be Red or Green.

    In Dotty, the future version of Scala, you can express that relatively directly as you wanted to:

    sealed trait Color
    sealed trait Red extends Color
    sealed trait Green extends Color
    sealed trait Blue extends Color
    case object Red extends Red
    case object Green extends Green
    case object Blue extends Blue
    
    trait Item {
      type Colors <: Color
    }
    
    object RedGreenItem extends Item {
      type Colors = Red | Green
    }
    
    def foo[I <: Item](item: I)(color: item.Colors): Unit = ()
    

    The extra sealed traits are for circumventing an other restriction that might still be lifted. But this works:

    scala> foo(RedGreenItem)(Red) 
    scala> foo(RedGreenItem)(Green) 
    scala> foo(RedGreenItem)(Blue) 
    -- [E007] Type Mismatch Error: <console>:13:18 ---------------------------------
    13 |foo(RedGreenItem)(Blue)
       |                  ^^^^
       |                  found:    Blue.type
       |                  required: RedGreenItem.Colors
       |     
    

    In current Scala I think the closest thing to model such a type member, in the general case, is with a Shapeless HList.

    import shapeless._, ops.hlist._
    
    trait Item {
      type Colors <: HList
    }
    
    object RedGreenItem extends Item {
      type Colors = Red.type :: Green.type :: HNil
    }
    
    def foo[I <: Item, C <: Color](item: I, color: C)(implicit contains: Selector[item.Colors, C]): Unit = ()
    

    And again this works as expected:

    scala> foo(RedGreenItem, Red)
    
    scala> foo(RedGreenItem, Green)
    
    scala> foo(RedGreenItem, Blue)
    <console>:27: error: Implicit not found: shapeless.Ops.Selector[RedGreenItem.Colors, Blue.type]. You requested an element of type Blue.type, but there is none in the HList RedGreenItem.Colors.
           foo(RedGreenItem, Blue)
              ^
    

    There are still other ways to express this. For instance with typeclasses instead of an overridable type member. I'm not recommending HLists over those, but that's how you can do it if you want to go with type members.