5

I started experimenting with ZIO, and was trying to run the following highly sophisticated program:

import logger.{Logger, _}
import zio.console._
import zio.{system, _}

object MyApp extends App {

  override def run(args: List[String]): ZIO[ZEnv, Nothing, Int] = {

    app
      .provideSome[Logger](_ => Slf4jLogger.create) //1 
      .fold(_ => 1, _ => 0)
  }

  val app: ZIO[Console with system.System with Logger, SecurityException, Unit] =
    for {
      _         <- info("This message from the logger") //2
      maybeUser <- system.env("USER")
      _         <- maybeUser match {
                      case Some(userName) => putStrLn(s"Hello ${userName}!")
                      case None => putStrLn("I don't know your name")
                    }
    } yield ()
}

(Slf4jLogger is https://github.com/NeQuissimus/zio-slf4j)

If I comment lines //1 and //2 out everything works fine. But in the above form I get a type mismatch error:

Error:(13, 45) type mismatch;
 found   : logger.Slf4jLogger
 required: zio.console.Console with zio.system.System with logger.Logger
      .provideSome[Logger](_ => Slf4jLogger.create)

I don't understand the following:

  1. The program required a Console and a System instance in the environment, yet I did not have to .provide it before, when I was not logging. Why? And why do I suddenly need it?

  2. According to the scaladoc, .provideSome Provides *some* of the environment required to run this effect, leaving the remainder R0. For me that means that I don't have to provide the full environment type, I could add the required modules one by one - yet it seems to expect the full ZEnv type, just like .provide - so what's the point?

  3. I cannot instantiate an anonymous class by new Logger with Console.Live with system.System.Live, because the Slf4jLogger has a factory method in the companion object. How can I use this factory method with the other traits?

  4. Since creating a logger instance in Java is kind of effectful, is .provide even the right function to call?

PS: I was able to get this working in a kind of ugly way:

    app
      .provideSome[ZEnv](_ =>
        new Console with Logger with system.System {
          override val console = Console.Live.console
          override val system = System.Live.system
          override val logger = Slf4jLogger.create.logger
        }
      ) //1
      .fold(_ => 1, _ => 0)

This compiles and runs, but overriding the mixed in traits' inner members does not seem right...

And it works in exactly the same way with .provide:

    app
      .provide(
        new Console with Logger with system.System {
          override val console = Console.Live.console
          override val system = System.Live.system
          override val logger = Slf4jLogger.create.logger
        }
      ) //1
      .fold(_ => 1, _ => 0)

What is going on?

haspok_adv
  • 53
  • 1
  • 5
  • If I comment out lines `//1` and `//2` in the original code I still have compile error `Error:(11, 12) type mismatch; found : zio.ZIO[zio.console.Console with zio.system.System with nequi.zio.logger.Logger,Nothing,Int] required: zio.ZIO[zio.ZEnv,Nothing,Int] (which expands to) zio.ZIO[zio.clock.Clock with zio.console.Console with zio.system.System with zio.random.Random with zio.blocking.Blocking,Nothing,Int] .fold(_ => 1, _ => 0)` – Dmytro Mitin Oct 25 '19 at 10:34

3 Answers3

6

I am afraid you do have to return an instance that implements all the traits you want to provide.

If you look at the example in ZIO's documentation for provideSome, you see that they are doing the exact same thing: They are taking a Console and turn it into a Console with Logging.

/**
   * Provides some of the environment required to run this effect,
   * leaving the remainder `R0`.
   *
   * {{{
   * val effect: ZIO[Console with Logging, Nothing, Unit] = ???
   *
   * effect.provideSome[Console](console =>
   *   new Console with Logging {
   *     val console = console
   *     val logging = new Logging {
   *       def log(line: String) = console.putStrLn(line)
   *     }
   *   }
   * )
   * }}}
   */

For me that means that I don't have to provide the full environment type, I could add the required modules one by one

No. You have to provide an instance of the full environment type to the effect you are wrapping (because it needs that). I think this is because we want to have this checked at compile-time and there is no way (without macros) to generate a "mash-up" stacked environment without spelling out how to do that. There will probably be some macros for that down the road.

just like .provide - so what's the point?

The difference is that with .provide you have to make up that instance of the required environment from scratch without any input. So that as a result, the effect (with your wrapping) no longer requires anything.

Whereas with .provideSome you yourself can request an environment to help you construct the instance you are providing. That environment will be passed into your function. In the example above, they require a Console and augment it to Console with Logging (by using the console instance they are given). As a result, effect.provideSome[Console](...) still requires a Console (whereas written with .provide it would require Nothing).

.provideSome[ZEnv](_ =>
        new Console with Logger with system.System {
          override val console = Console.Live.console

Since you are using provideSome[ZEnv], you should probably not access the global singletons Console.Live, but take that from the env being passed in:

.provideSome[ZEnv](env =>
        new Console with Logger with system.System {
          override val console = env.console

The program required a Console and a System instance in the environment, yet I did not have to .provide it before

That is because ZEnv, the default environment already provides these two.

Since creating a logger instance in Java is kind of effectful, is .provide even the right function to call?

Personally, I would ignore that constructing loggers is effectful. It does not seem like something worth tracking. I consider it some basic facility that it is almost a part of class-loading.

If you did want to track the effect, you could do

app
  .provideSomeM(env => for {
     logger <- UIO(Slf4jLogger.create)
   } yield new MyEnvironment(env, logger)
  )
Thilo
  • 257,207
  • 101
  • 511
  • 656
  • For macros to help with the boilerplate, look at https://github.com/zio/zio-macros/blob/master/docs/mix.md – Thilo Oct 26 '19 at 01:08
1

Let me try to explain.

If you have a look at what ZEnv is it's an alias for Clock with Console with System with Random with Blocking. So, when you started implementing def run(args: List[String]): ZIO[ZEnv, Nothing, Int] zio already provided you with these capabilities.

Your app variable signature has a return type of zio with environment Console with System with Logger - one thing that stands out is a Logger capability. It's not standard so ZIO can't provide it for you. You have to provide it yourself.

That's what you did using .provideSome[Logger] - you eliminated one of the capabilities from environment requirements making it type compatible with the standard:

type ZEnv = Clock with Console with System with Random with Blocking
type YourEnv = Console with System
type Proof = ZEnv <:< YourEnv
Alex
  • 7,460
  • 2
  • 40
  • 51
0

This is a way that I use:

import logger.{Logger, _}
import zio.console._
import zio.{system, _}

object MyApp extends App {

  override def run(args: List[String]): ZIO[ZEnv, Nothing, Int] = {

    app
      .fold(_ => 1, _ => 0)
  }

  val app: ZIO[Environment, SecurityException, Unit] =
    for {
      _         <- info("This message from the logger").provide( Slf4jLogger.create) // 1
      maybeUser <- system.env("USER")
      _         <- maybeUser match {
                      case Some(userName) => putStrLn(s"Hello ${userName}!")
                      case None => putStrLn("I don't know your name")
                    }
    } yield ()
}
  1. I provide it directly in the for-comprehension of the app. All the rest you have from the zio.App.
pme
  • 14,156
  • 3
  • 52
  • 95