Search code examples
scalaruntimelayerzio

Why override bootstrap, not work for run method?


I'm learning ZIO 2.x, when configuring Runtime using bootstrap layer, it don't works.

object RuntimeCustom extends ZIOAppDefault {

  // It's not work, And I don't know why?
  override val bootstrap = EmailService.live
  
  def run = (for {
    _ <- ZIO.debug("Start...")
    _ <- EmailService.send("God", "Hi")
    _ <- ZIO.debug("End...")
  } yield ())
}

Get one error:

[error] /Users/changzhi/github-repo/zio-start/src/main/scala/zio/reference/experiment/core/RuntimeCustom.scala:35:7: 
[error] 
[error] ──── ZIO APP ERROR ───────────────────────────────────────────────────
[error] 
[error]  Your effect requires a service that is not in the environment.
[error]  Please provide a layer for the following type:
[error] 
[error]    1. example.EmailService
[error] 
[error]  Call your effect's provide method with the layers you need.
[error]  You can read more about layers and providing services here:
[error]  
[error]    https://zio.dev/next/datatypes/contextual/
[error] 
[error] ──────────────────────────────────────────────────────────────────────
[error] 
[error]     _ <- EmailService.send("God", "Hi")
[error]       ^
[error] one error found
[error] (Compile / compileIncremental) Compilation failed

If I replace with provide, it works.

object RuntimeCustom extends ZIOAppDefault {
  
  def run = (for {
    _ <- ZIO.debug("Start...")
    _ <- EmailService.send("God", "Hi")
    _ <- ZIO.debug("End...")
  } yield ())
    // This can works, have no doubt
    .provide(EmailService.live)
}

Full version program is here

package example

import zio._
trait EmailService {
  def send(user: String, content: String): Task[Unit]
}
object EmailService {
  def send(user: String, content: String): ZIO[EmailService, Throwable, Unit] =
    ZIO.serviceWithZIO[EmailService](_.send(user, content))

  val live: ZLayer[Any, Nothing, EmailService] = 
    ZLayer.fromZIO( ZIO.succeed(EmailServiceFake()) <* Console.printLine("Init EmailService") ).orDie
}

case class EmailServiceFake() extends EmailService {
  override def send(user: String, content: String): Task[Unit] =
    Console.printLine(s"sending email to $user")
}

object RuntimeCustom extends ZIOAppDefault {

  
  // It's not work, And I don't know why?
  //override val bootstrap = EmailService.live
  
  def run = (for {
    _ <- ZIO.debug("Start...")
    _ <- EmailService.send("God", "Hi")
    _ <- ZIO.debug("End...")
  } yield ())
    // This can works, have no doubt
    .provide(EmailService.live)
}

Solution

  • ZIOAppDefault is meant for apps that will not require additional services, the assumption is that they will only "require" the built in services because you have provided away your other requirements with the provide method.

    If you want to use the bootstrap method you should instead extend ZIOApp and override both the environmentTag and Environment fields so that the run mechanism knows how to construct the final environment.

    import zio._
    
    object App extends ZIOApp {
      
      // Tell ZIO how the environment is constructed
      override val environmentTag: EnvironmentTag[Environment] = EnvironmentTag[Environment]
      
      // Tell the app which layers will be leftover from the `run`
      override type Environment = FooService
      
      // The app how to construct those remaining layers
      override val bootstrap = FooService.layer
      
      
      val run = FooService.doFoo
      
      
    }
    
    class FooService {
      def doFoo: UIO[Unit] = ZIO.unit
    }
    
    object FooService {
      val layer = ZLayer.succeed(new FooService)
      
      def doFoo = ZIO.serviceWithZIO[FooService](_.doFoo)
    }
    

    Edit

    Using the original example to show that this works:

    import zio._
    
    
    trait EmailService {
      def send(user: String, content: String): Task[Unit]
    }
    object EmailService {
      def send(user: String, content: String): ZIO[EmailService, Throwable, Unit] =
        ZIO.serviceWithZIO[EmailService](_.send(user, content))
    
      val live: ZLayer[Any, Nothing, EmailService] = 
        ZLayer.fromZIO( ZIO.succeed(EmailServiceFake()) <* Console.printLine("Init EmailService") ).orDie
    }
    
    case class EmailServiceFake() extends EmailService {
      override def send(user: String, content: String): Task[Unit] =
        Console.printLine(s"sending email to $user")
    }
    
    object RuntimeCustom extends ZIOApp {
    
      
      // It's not work, And I don't know why?
      override val bootstrap = EmailService.live 
      
      override type Environment = EmailService
      
      override val environmentTag: EnvironmentTag[Environment] = EnvironmentTag[Environment]
      
      
      def run = (for {
        _ <- ZIO.debug("Start...")
        _ <- EmailService.send("God", "Hi")
        _ <- ZIO.debug("End...")
      } yield ())
    }
    

    https://scastie.scala-lang.org/02MMaWS2S6Wwe55a24H7Sg