scalascala-3zio

Can't get tagged types to compile inside ZLayer


I have two database connections that I want to be able to separate. I found this post that describes how to implement Shapeless style tagged types in Scala 3. However, when I try to create a ZLayer with a tagged value, I can never satisfy the implicits. Tried every combination I could think of with typed methods and/or passing type parameters - same result all the time.

For the sake of simplicity, I'll just give example code trying to tag String instead.

object TaggedTypes {
  opaque type Tagged[+V, +Tag] = Any
  type @@[+V, +Tag] = V & Tagged[V, Tag]

  def tag[Tag]: [V] => V => V @@ Tag =
    [V] => (v: V) => v
}

import TaggedTypes.*
trait Tag1
trait Tag2

val layer1: ULayer[String @@ Tag1] = ZLayer.succeed(tag[Tag1]("string1"))
val layer2: ULayer[String @@ Tag2] = ZLayer.succeed(tag[Tag2]("string2"))

val program: UIO[Unit] = for {
  string1 <- ZIO.service[String @@ Tag1]
  string2 <- ZIO.service[String @@ Tag2]
  _ <- zio.Console.printLine(s"This should have value 'string1': ${string1}")
  _ <- zio.Console.printLine(s"This should have value 'string2': ${string2}")
} yield ()

If program would be run, I would expect values to match as in console log statements.

Instead, compilation fails with:

could not find implicit value for izumi.reflect.Tag[(String & TaggedTypes.Tagged[String, Tag1])].
Did you forget to put on a Tag, TagK or TagKK context bound on one of the parameters in (String & TaggedTypes.Tagged[String, Tag1])? e.g. def x[T: Tag, F[_]: TagK] = .... 
I found:  zio.Tag.materialize[ (String & TaggedTypes.Tagged[String, Tag1]) ]  
But method materialize in trait TagVersionSpecific does not match type zio.Tag[(String & TaggedTypes.Tagged[String, Tag1]) ].

zio is on version 2.0.13, izumi-reflect is on version 2.3.7


Solution

  • To do what you are trying to do here isn't really the way that ZIO wants you to handle this. Its mostly possible except you need to have a type tag for each connection (String in the example) that is unique, which I'm not sure how to do.

    The code below will always print string2 because TaggedString[T] always returns the same Tag and thus the write to the environment Map is overwritten.

    import izumi.reflect.dottyreflection.ReflectionUtil.reflectiveUncheckedNonOverloadedSelectable
    import zio.{Tag, UIO, ULayer, ZEnvironment, ZIO, ZIOAppDefault, ZLayer}
    
    import java.io.IOException
    
    opaque type Tagged[+V, +Tag] = Any
    type @@[+V, +Tag] = V & Tagged[V, Tag]
    
    def tag[Tag]: [V] => V => V @@ Tag = [V] => (v: V) => v
    
    trait Tag1
    trait Tag2
    
    object TagExample extends ZIOAppDefault {
      override def run: ZIO[Any, IOException, Unit] = {
        // The problem is to express one of these that is unique for each Tag
        given TaggedString[T]: Tag[String @@ T] = Tag[String].asInstanceOf[Tag[String @@ T]]
    
        val a: String @@ Tag1 = tag[Tag1]("string1")
        val b: String @@ Tag2 = tag[Tag2]("string2")
    
        val layer1: ULayer[String @@ Tag1] = ZLayer.succeed[String @@ Tag1](a)
        val layer2: ULayer[String @@ Tag2] = ZLayer.succeed[String @@ Tag2](b)
    
        val program: ZIO[String @@ Tag1 & String @@ Tag2, IOException, Unit] = for {
           string1 <- ZIO.service[String @@ Tag1]
           string2 <- ZIO.service[String @@ Tag2]
           _ <- zio.Console.printLine(s"This should have value 'string1': ${string1}")
           _ <- zio.Console.printLine(s"This should have value 'string2': ${string2}")
        } yield ()
    
        program.provideLayer(layer1 ++ layer2)
      }
    }
    

    However, its MUCH simpler to just wrap the connections in a case class to give them separate types.

    This example works properly:

    import zio.{UIO, ULayer, ZEnvironment, ZIO, ZIOAppDefault, ZLayer}
    
    import java.io.IOException
    
    case class String1(value: String)
    case class String2(value: String)
    
    object TagExample extends ZIOAppDefault {
      override def run: ZIO[Any, IOException, Unit] = {
        val layer1: ULayer[String1] = ZLayer.succeed(String1("string1"))
        val layer2: ULayer[String2] = ZLayer.succeed(String2("string2"))
    
        val program: ZIO[String1 & String2, IOException, Unit] = for {
          string1 <- ZIO.service[String1]
          string2 <- ZIO.service[String2]
          _ <- zio.Console.printLine(s"This should have value 'string1': ${string1.value}")
          _ <- zio.Console.printLine(s"This should have value 'string2': ${string2.value}")
        } yield ()
    
        program.provideLayer(layer1 ++ layer2)
      }
    }