Is it possible to replace a specific layer(s) in a ZIO effect and keep the rest of the environment?

163 views Asked by At

ChatGPT says this should work, but looks like it is mistaken :/

      trait Foo { def s: String }
      case object A extends Foo { def s = "A" }
      case object B extends Foo { def s = "B" }
      test("Replace a service in ZIO") {
        val layer = ZLayer.succeed[Foo](A)
        val otherLayer = ZLayer.succeed[Foo](B)
        val app = (for {
          foo <- ZIO.service[Foo]
          bar <- ZIO.service[String]
        } yield foo.s + bar).provide(layer, ZLayer.succeed("foo"))

        val otherApp = app.provideSomeLayer(otherLayer)
        otherApp.map { s => assertTrue(s == "Bfoo") }
      }

I have an app here that has service A and string "foo". Trying to create the otherApp from it, replacing A with B, and keeping the rest ("foo") as is. But no luck - this test fails because the resulting value is still "Afoo" rather than "Bfoo".

What am I doing wrong?

Also (on a sort of different topic), if I change provide there to provideLayer(layer).provide(ZLayer.succeed("foo"), it no longer compiles saying it wants layer to be Foo with String. Is there no way to provide some dependencies to the effect, and leave others undefined?

And one more noob question, while I have your attention. At which point do these ZIOs actually run? Does this test run them both? Or will it only run the otherApp because it is the only one that is used to produce the return value?

1

There are 1 answers

4
Gaël J On

Trying to create the otherApp from it, replacing A with B, and keeping the rest ("foo") as is.

I don't think you can "replace" a layer once it's been provided.

I'm actually even surprised that your usage of provideSomeLayer doesn't raise an error because you provided too much layers. Usually it fails to compile when a unnecessary layer is provided.


Is there no way to provide some dependencies to the effect, and leave others undefined?

Yes, provideSome will do it:

val program: ZIO[Foo with String, Nothing, String] = (for {
  foo <- ZIO.service[Foo]
  bar <- ZIO.service[String]
} yield foo.s + bar)

// Providing only the String type, and leaving Foo to be defined later
val appPartial: ZIO[Foo, Nothing, String] = program.provideSome[Foo](ZLayer.succeed("foo"))

// Defining Foo now with B
val otherApp: ZIO[Any, Nothing, String] = appPartial.provide(otherLayer)

At which point do these ZIOs actually run?

If you were outside a ZIO test, nothing would run until you give the ZIO to a "runtime" with something like this:

Unsafe.unsafe { implicit unsafe =>
  zio.Runtime.default.unsafe.run(otherApp).getOrThrowFiberFailure()
}

But in a ZIO test context, the framework runs the effects for you. So you don't need this "runtime" thing. And I assume that it only runs the effect of the returned value.