zio / zio-prelude

A lightweight, distinctly Scala take on functional abstractions, with tight ZIO integration
https://zio.dev/zio-prelude
Apache License 2.0
451 stars 115 forks source link

ZEnvironment#prune doesn't work with newtypes on scala 2 #1341

Open paulpdaniels opened 4 months ago

paulpdaniels commented 4 months ago

This is more of an aspirational issue as I'm not sure if this is actually fixable. At issue is the fact that Newtype[A] will fail at runtime if it interacts with ZEnvironment#prune.

Consider:

object TestApp extends ZIOAppDefault {

  case class Wrapped[A](value: A)

  object MyType1 extends Newtype[Wrapped[Int]] {
    implicit val tag: Tag[Type] = derive[Tag]
    val layer: ULayer[Type]     = ZLayer.succeed[Type](wrap(Wrapped(42)))
  }
  type MyType1 = MyType1.Type

  object MyType2 extends Newtype[Wrapped[String]] {
    implicit val tag: Tag[Type] = derive[Tag]
    val layer: ULayer[Type]     = ZLayer.succeed[Type](wrap(Wrapped("42")))
  }
  type MyType2 = MyType2.Type

  val run = (for {
    env <- ZIO.environment[MyType1 with MyType2]
    _ = env.prune
  } yield ()).provide(
    MyType1.layer,
    MyType2.layer
  )

}

This will fail with Defect in zio.ZEnvironment: HashSet(TestApp::MyType1::Type, TestApp::MyType2::Type) statically known to be contained within the environment are missing

You can artificially make this work by explicitly providing a cast EnvironmentTag

  val envTag: EnvironmentTag[MyType1 with MyType2] =
    EnvironmentTag[MyType1.Wrapped with MyType2.Wrapped]
      .asInstanceOf[EnvironmentTag[MyType1 with MyType2]]

   env.prune(envTag)

Moreover as long as you don't invoke a prune it will function correctly.

for instance:

  val run = (for {
    myType1 <- ZIO.service[MyType1]
    myType2 <- ZIO.service[MyType2]
  } yield ()

I suspect the issue arises because of how type tagging occurs. During creation of a layer we use the derive mechanism which is essentially just implicitly[Tag[Wrapped]].asInstanceOf[Tag[Type]], this means that the LightTagType that is inserted into the type map is that of the underlying type. In most cases the get operation then uses the same mechanism, that is fetching by the underlying type id. However, in the prune case it is generating an intersection type via macro which seems to hold onto ephemeral type of the newtype instead. Hence, when it iterates through the set of environmental types it can't find them (because it isn't referencing the aliased type anymore). This is just a suspicion though, I don't have enough experience with the internals of this to say for certain.

If this is actually an impossible ask, I would suggest adding a disclaimer to the docs indicating that newtypes aren't usable in this fashion, since this particular issue occurs at runtime.

finalchild commented 4 months ago

Also occurs in Scala 3

kyri-petrou commented 4 months ago

Moreover as long as you don't invoke a prune it will function correctly

This is the part that I'm finding most bizarre. I'm wondering whether this might be a legit bug either in how we do subtype checking in ZIO during pruning, or in izumi-tag