Search code examples
scalagenericsinner-classespath-dependent-type

How to achieve argument and path dependent type parametrization in constructor?


(using scala 2.11.12)

Why does this compile?

  sealed trait Inner

  sealed trait Outer {
    sealed trait I extends Inner
  }

  case object OuterA extends Outer {
    case object Inner1 extends I
    case object Inner2 extends I
  }

  case object OuterB extends Outer {
    case object Inner1 extends I
  }

  class Data[O <: Outer](outer: O, inner: O#I)
  // I expected this not to compile but it actually does
  val data = new Data(OuterA, OuterB.Inner1)

Why does this not compile?

  sealed trait Inner

  sealed trait Outer {
    type I <: Inner
  }

  case object OuterA extends Outer {
    sealed trait OuterAInner extends Inner
    override type I = OuterAInner
    case object Inner1 extends OuterAInner
    case object Inner2 extends OuterAInner
  }

  case object OuterB extends Outer {
    sealed trait OuterBInner extends Inner
    override type I = OuterBInner
    case object Inner1 extends OuterBInner
  }

  class Data[O <: Outer](outer: O, inner: O#I)
  // I expected this to compile but it actually does not
  val data = new Data(OuterA, OuterA.Inner1)
  // type mismatch;
  //   found   : com.transparencyrights.ermine.model.V1.OuterA.Inner1.type
  //   required: ?#I
  //  Note that Inner1 extends Any, not AnyRef.
  //  Such types can participate in value classes, but instances
  //  cannot appear in singleton types or in reference comparisons.
  //    val data = new Data(OuterA, OuterA.Inner1)

What I want to achieve is a unique Data constructor that takes two arguments, an Outer and an Inner, with the Inner type restricted to an Inner subtype dependent on the given Outer instance.


Solution

  • On both cases the problem is that O#I does not do what you want it to do.
    It really does not refer to the I inside the specific O you have, but rather to the generic one inside Outer.

    You can fix both snippets using Path dependent types and Generalized type constraints.

    Case 1

    sealed trait Inner
    
    sealed trait Outer {
      sealed trait I extends Inner
    }
    
    final case object OuterA extends Outer {
      final case object Inner1 extends I
      final case object Inner2 extends I
    }
    
    final case object OuterB extends Outer {
      final case object Inner1 extends I
    }
    
    final class Data[O <: Outer, I <: O#I] private (outer: O, inner: I)
    
    object Data {
      final def apply[O <: Outer, I <: O#I](outer: O, inner: I)(implicit ev: I <:< outer.I): Data[O, I] =
        new Data(outer, inner)
    }
    
    val data = Data(OuterA, OuterB.Inner1) // Does not compile.
    val data = Data(OuterA, OuterA.Inner1) // Does compile.
    

    Case 2

    sealed trait Inner
    
    sealed trait Outer {
      type I <: Inner
    }
    
    final case object OuterA extends Outer {
      override final type I = OuterAInner
    
      sealed trait OuterAInner extends Inner
    
      final case object Inner1 extends OuterAInner
      final case object Inner2 extends OuterAInner
    }
    
    final case object OuterB extends Outer {
      override final type I = OuterBInner
    
      sealed trait OuterBInner extends Inner
    
      final case object Inner1 extends OuterBInner
    }
    
    final class Data[O <: Outer, I <: O#I] private (outer: O, inner: I)
    
    object Data {
      final def apply[O <: Outer, I <: O#I](outer: O, inner: I)(implicit ev: I <:< outer.I): Data[O, I] =
        new Data(outer, inner)
    }
    
    val data = new Data(OuterA, OuterA.Inner1) // Does compile.
    val data = new Data(OuterA, OuterB.Inner1) // Does not compile.
    

    Now, since the examples does not show how do you want to use the Data class and the outer & inner fields, this may not be enough, but hope it helps you.
    If you have any doubts, do not hesitate on asking on the comments.