eirirlar eirirlar - 1 month ago 8
Scala Question

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)

Answer

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.

Comments