scalagenericstype-parametertype-aliastype-members

Implicitly wrapped trait with type member does not compile


The following does not compile because of the last line:

object ImplicitWrappedTraitWithType {

  trait Wrapper[+T] {
    def unwrap: T
  }

  object Wrapper {
    def apply[T](implicit w: Wrapper[T]): Wrapper[T] = w
  }

  trait IO[In] {
    type Out

    def out: Out
  }

  implicit def decoder[In]: Wrapper[IO[In] {type Out = String}] = new Wrapper[IO[In] {type Out = String}] {
    override def unwrap: IO[In] {type Out = String} = new IO[In] {
      override type Out = String
      override val out: Out = "yeah"
    }
  }

  val wrap = Wrapper[IO[String]]
  val io: IO[String] = wrap.unwrap
  val out: String = io.out //actual type: unwrap.Out
}

What can I do to convince the compiler that val out is a String?


Pre-edit - ignore this

Example 1 - this does not compile:

object ImplicitWrappedTraitWithType {
  class Wrapper[T]
  object Wrapper {
    def apply[T](implicit w: Wrapper[T]): Wrapper[T] = w
  }
  trait IO[In] {
    type Out
  }
  implicit def decoder[In]: Wrapper[IO[In] {type Out = String}] = null

//client code
  Wrapper[IO[String]]
}

Example 2 - whereas this does:

object ImplicitWrappedTraitWithType {
  class Wrapper[T]
  object Wrapper {
    def apply[T](implicit w: Wrapper[T]): Wrapper[T] = w
  }
  trait IO[In] {
    type Out
  }
  implicit def decoder[In]: Wrapper[IO[In]] = null

//client code
  Wrapper[IO[String]]
}

In the client code I don't know what the type of Out will be, but I need to be able to access it when I extract an instance of IO from Wrapper (code for that not shown).

How must 'Example 1' be changed for this to compile, while retaining the Out parameter in a way that is visible for the client code.

(Please comment if this formulation is unclear)


Solution

  • You only need two minor edits to your code.

    trait Wrapper[+T] {
      def unwrap: T
    }
    
    object Wrapper {
      def apply[T](implicit w: Wrapper[T]): w.type = w
    }
    
    trait IO[In] {
      type Out
    
      def out: Out
    }
    
    implicit def decoder[In]: Wrapper[IO[In] {type Out = String}] = new Wrapper[IO[In] {type Out = String}] {
      override def unwrap: IO[In] {type Out = String} = new IO[In] {
        override type Out = String
        override val out: Out = "yeah"
      }
    }
    
    val wrap = Wrapper[IO[String]]
    val io = wrap.unwrap
    val out: String = io.out
    

    The most important is to change the return type of the apply method to w.type. That way the full type of w (including all refinements) is retained. If you write Wrapper[T] as return type and you ask for a Wrapper[T] for T equal to IO[String], you will get a Wrapper[IO[String]] and all extra knowledge like {type Out = String} will be lost.

    Second: in val io: IO[String] = wrap.unwrap you say that io is an IO[String]. Again, all extra knowledge is lost. So just let the compiler infer the type of io.

    Another thing: if you don't want Wrapper to be covariant in T, you can just leave off the variance annotation and change your apply method.

    trait Wrapper[T] {
      def unwrap: T
    }
    
    object Wrapper {
      def apply[T](implicit w: Wrapper[_ <: T]): w.type = w
    }
    

    That way the compiler still knows it has to look for a subtype of IO[String] if you call Wrapper.apply[IO[String]]. Because IO[String]{type out = String} is a subtype of IO[String], they are not equal.