From 9e184bed51f63b634fcb8467af27ffe3a8cb7998 Mon Sep 17 00:00:00 2001 From: Milad Khajavi Date: Sun, 11 Dec 2022 19:06:18 +0330 Subject: [PATCH 1/3] zio environment use-cases. --- .../contextual/zio-environment-use-cases.md | 141 ++++++++++++++++++ website/sidebars.js | 1 + 2 files changed, 142 insertions(+) create mode 100644 docs/reference/contextual/zio-environment-use-cases.md diff --git a/docs/reference/contextual/zio-environment-use-cases.md b/docs/reference/contextual/zio-environment-use-cases.md new file mode 100644 index 000000000000..155d19508f0f --- /dev/null +++ b/docs/reference/contextual/zio-environment-use-cases.md @@ -0,0 +1,141 @@ +--- +id: zio-environment-use-cases +title: "ZIO Environment Use-cases" +--- + +ZIO Environment has two major use-cases: + +1. Local Capabilities like scopes and transactions. +2. Business Logic like services and repositories. + +## Local Capabilities + +The most idiomatic use of the ZIO environment is for: + +- Describing as values workflows that use capabilities that are "local" to a particular context +- Where tracking the use of this context at the type level is helpful for reasoning about programs + +Let's look at [`Scope`](../resource/scope.md), which I would say is the most idiomatic usage of the environment in ZIO itself, to see what each of these mean: + +### 1. Local Contexts + +The first criteria is that a capability be "local" to a particular context and not shared throughout the entire application. For example, in the case of `Scope` we typically don't just have one scope for our entire application but many smaller scopes, such as the scope for using a particular file. Usages of local context also have an operator that allows locally eliminating this capability, such as the `ZIO.scoped` operator which transforms a `ZIO[R with Scope, E, A]` to a `ZIO[R, E, A]`. + +Other potential examples of this would be the [`ZState`](../state-management/zstate.md) data type in ZIO, which describes some local use of state and is eliminated by the `ZIO.stateful` operator, a `Transaction` representing the usage of a database transaction, or the context of a particular HTTP request (`RequestContext`). + +Notice that using the environment type allows workflows that require these capabilities to be first class values that **compose naturally** and can have their own operators for working with them, which would not be possible if we defined them as methods that required us to explicitly pass around these capabilities. + +### 2. Type-level Reasoning + +The second criteria is that tracking the usage of this context be helpful for reasoning about programs. We can also use a [`FiberRef`](../state-management/fiberref.md) value to maintain some local context. However, when we do so, our usage of that context is not reflected at the type level. + +For example, when we log something it will use a variety of contextual information including the current log level, the current log span, and the current log annotations but none of this is reflected at the type level: + +```scala +object ZIO { + def log(message: => String)(implicit trace: Trace): ZIO[Any, Nothing, Unit] +} +``` + +Not reflecting this usage of contextual information at the type level can be both an advantage and a disadvantage: + +- The advantage is that it can create a simpler API because we do not clutter up the environment with additional dependencies. +- The disadvantage is that we can't track at the type level whether we are using contextual information or whether we have provided it. + +In the case of logging this is clearly the right trade-off. Logging is a low level concern that we don't want to require us to update our type signatures, and there is essentially no harm in running a ZIO workflow that does logging without providing this log context since we can just log at some default log level without any log spans or annotations. + +In contrast, in the case of `Scope` there is tremendous value in reflecting the use of `Scope` at the type level so we know whether a workflow is resourceful and can have operators that reflect at the type level that we have provided a `Scope` to part of our application. Similarly if we have a **database transaction** reflecting at the type level that some workflow needs to be done as part of a transaction and when we are "executing" a transaction is extremely valuable. + +## Business Logic + +The other potential use of the ZIO environment is describing the dependencies of our business logic itself. Normally, we implement higher level services in terms of lower level services using [constructor based dependency injection with ZLayer](../di/index.md). + +```scala mdoc:silent +import zio._ + +trait HighLevelService { + def doSomething: ZIO[Any, Nothing, Unit] +} + +object HighLevelService { + val live: ZLayer[LowLevelService, Nothing, HighLevelService] = + ZLayer.fromFunction(HighLevelServiceLive(_)) + + final case class HighLevelServiceLive(lowLevelService: LowLevelService) extends HighLevelService { + def doSomething: ZIO[Any, Nothing, Unit] = + ??? // implemented in terms of `LowLevelService` + } +} + +trait LowLevelService { + def doSomethingElse: ZIO[Any, Nothing, Unit] +} + +object LowLevelService { + val live: ZLayer[Any, Nothing, LowLevelService] = + ??? +} +``` + +This allows us to avoid using `LowLevelService` in the environment and to not "leak" implementation details, since the dependency on `LowLevelService` is an implementation detail of `HighLevelService` that might not even exist if `HighLevelService` is refactored. + +However, the question arises then of how we should work with `HighLevelService` in the core of our business logic or the center of the "onion" in the [onion architecture](../architecture/architectural-patterns.md#onion-architecture)? + +There are two approaches to this. + +### Everything as a Service + +The first approach is just that everything is a service: + +```scala mdoc:compile-only +sealed trait ApplicationService { + def run: ZIO[Any, Nothing, Unit] +} + +object ApplicationService { + + final case class ApplicationServiceLive(highLevelService: HighLevelService) extends ApplicationService { + val run: ZIO[Any, Nothing, Unit] = ??? // business logic implemented in terms of high level services + } +} + +object Main extends ZIOAppDefault { + val run = + ZIO + .serviceWithZIO[ApplicationService(_.run) + .provide( + ApplicationService.live, + HighLevelSevice.live, + LowLevelService.live + ) +} +``` + +This style avoids any usage of the ZIO environment that is not a local capability except for possibly a single time within `ZIOAppDefault`. This way there is no need to implement [service accessors](../service-pattern/service-pattern.md#5-accessor-methods), except for potentially writing tests, and there is a certain conceptual regularity that everything is a service. + +### Using ZIO Environment + +However, there can be a feeling that defining this final `ApplicationLevelService` is unnecessary and we would like to be able to write our business logic in terms of high level services directly without making it another service: + +```scala mdoc:silent +import zio._ + +object Main extends ZIOAppDefault { + + val myProgramLogic: ZIO[HighLevelService, Nothing, Unit] = + for { + _ <- ZIO.serviceWithZIO[HighLevelService](_.doSomething) + - <- otherLogicHere + } yield () + + val run = + myProgramLogic.provide( + HighLevelSevice.live, + LowLevelService.live + ) +} +``` + +There has been some movement towards the "everything is a service" approach since it avoids the need to implement service accessors but it can be a matter of team style which of these approaches to use. Either way our program is the same except for whether in our business logic we call methods on services directly or use the environment for that. + +To learn more about this approach please see [how we can use dependency injection with the service pattern](../di/dependency-injection-in-zio.md#dependency-injection-and-service-pattern). \ No newline at end of file diff --git a/website/sidebars.js b/website/sidebars.js index 0e4af7b7cf3d..7ac52e602ea8 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -177,6 +177,7 @@ module.exports = { items: [ "reference/contextual/zenvironment", + "reference/contextual/zio-environment-use-cases", { type: "category", label: "ZIO Layers", From 3f4f329a1646a3115d85d2ff319d46d5b3430c67 Mon Sep 17 00:00:00 2001 From: Milad Khajavi Date: Mon, 12 Dec 2022 09:25:56 +0330 Subject: [PATCH 2/3] clean up. --- .../contextual/zio-environment-use-cases.md | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/reference/contextual/zio-environment-use-cases.md b/docs/reference/contextual/zio-environment-use-cases.md index 155d19508f0f..a376b62f89e0 100644 --- a/docs/reference/contextual/zio-environment-use-cases.md +++ b/docs/reference/contextual/zio-environment-use-cases.md @@ -3,10 +3,12 @@ id: zio-environment-use-cases title: "ZIO Environment Use-cases" --- -ZIO Environment has two major use-cases: +ZIO Environment allows us to describe workflows which carry some context that is used in the course of executing the workflow. This context can be dived into two categories: -1. Local Capabilities like scopes and transactions. -2. Business Logic like services and repositories. +1. Local Capabilities, e.g. scopes and transactions +2. Business Logic, e.g. services and repositories + +Let's discuss each of these in turn. ## Local Capabilities @@ -72,8 +74,7 @@ trait LowLevelService { } object LowLevelService { - val live: ZLayer[Any, Nothing, LowLevelService] = - ??? + val live: ZLayer[Any, Nothing, LowLevelService] = ??? } ``` @@ -87,12 +88,13 @@ There are two approaches to this. The first approach is just that everything is a service: -```scala mdoc:compile-only +```scala sealed trait ApplicationService { def run: ZIO[Any, Nothing, Unit] } object ApplicationService { + val live: ZLayer[Any, Nothing, LowLevelService] = ??? final case class ApplicationServiceLive(highLevelService: HighLevelService) extends ApplicationService { val run: ZIO[Any, Nothing, Unit] = ??? // business logic implemented in terms of high level services @@ -102,10 +104,10 @@ object ApplicationService { object Main extends ZIOAppDefault { val run = ZIO - .serviceWithZIO[ApplicationService(_.run) + .serviceWithZIO[ApplicationService](_.run) .provide( ApplicationService.live, - HighLevelSevice.live, + HighLevelService.live, LowLevelService.live ) } @@ -117,7 +119,7 @@ This style avoids any usage of the ZIO environment that is not a local capabilit However, there can be a feeling that defining this final `ApplicationLevelService` is unnecessary and we would like to be able to write our business logic in terms of high level services directly without making it another service: -```scala mdoc:silent +```scala import zio._ object Main extends ZIOAppDefault { @@ -138,4 +140,4 @@ object Main extends ZIOAppDefault { There has been some movement towards the "everything is a service" approach since it avoids the need to implement service accessors but it can be a matter of team style which of these approaches to use. Either way our program is the same except for whether in our business logic we call methods on services directly or use the environment for that. -To learn more about this approach please see [how we can use dependency injection with the service pattern](../di/dependency-injection-in-zio.md#dependency-injection-and-service-pattern). \ No newline at end of file +To learn more about this approach please see [how we can use dependency injection with the service pattern](../di/dependency-injection-in-zio.md#dependency-injection-and-service-pattern). From 36ca65fde328194b608eb74159b895962b70143a Mon Sep 17 00:00:00 2001 From: Milad Khajavi Date: Mon, 12 Dec 2022 10:09:34 +0330 Subject: [PATCH 3/3] zio environment vs. fiberref. --- docs/about/faq.md | 78 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/docs/about/faq.md b/docs/about/faq.md index fbc45d0104ae..5bff638e0748 100644 --- a/docs/about/faq.md +++ b/docs/about/faq.md @@ -6,6 +6,84 @@ sidebar_label: "FAQ" In this page we are going to answer general questions related to the ZIO project. +## Where should we encode contextual values like `UserId`, `CorrelationId` in my ZIO application? + +1. Should we put `CorrelationId` and `UserId` as well into a `FiberLocal`? +2. Should our effects be something like `val someEffect: ZIO[CorrerlationId & UserId, ErrorType, A]`? +3. Should we keep writing our effects with explicit params as `def someEffect(c: CorrelationId, u: UserId, params...): ZIO[Any, ErrorType, A]`? +4. Should we put these context parameters as implicits, like `def someEffect(params..)(implicit c: CorrelationId, u: UserId): ZIO[Any, ErrorType, A]`? + +Before answering these question, make sure you have read the [ZIO Environment Use-cases](../reference/contextual/index.md) section. + +Now, let's go into this in a little more detail. We have some workflow, `someEffect` that conceptually requires both a `CorrelationId` and a `UserId` to be run. Let's consider any of these solutions in turn. + +### Solution 1: Explicit Parameters + +The simplest way to model this dependency is just as function parameters: + +```scala +def someEffect(userId: UserId, correlationId: CorrelationId, ...): ZIO[Any, ErrorType, A] = + ??? +``` + +This is a fine starting point. Functions that take arguments are about as simple as you can get. However, there are two main issues with this. + +First, it can just become a lot of boilerplate. The signature above looks potentially okay on its own, but if it is being called by some other function which in turn is being called by some other function it can become a lot of boilerplate very fast which is why people turn to all of these alternatives. I don't think there is a hard and fast rule for when it gets to be "too much" boilerplate but if you are unsure I think a good practice can be to use explicit parameters until you "feel the pain" and then you can refactor to one of the alternatives discussed below. + +The second problem with this approach is that someEffect is a method and not a first class value, which can limit our ability to work with it in some cases. But typically the boilerplate is the overwhelming problem that leads people to move away from this approach. + +### Solution 2: Implicit Parameters + +The second alternative you highlight is to make these parameters implicit. + +```scala +def someEffect(...)(implicit userId: UserId, correlationId: CorrelationId): ZIO[Any, ErrorType, A] = + ??? +``` + +This addresses the boilerplate problem with explicit parameters by allowing us to pass them implicitly. We would basically never recommend this solution. + +To reason about implicit values in a principled way we want them to be "coherent" which means there is only one implicit value corresponding to any type within our entire program. For example, there is only one Associative instance for String if we are using functional abstractions, or there is only one `JsonEncoder` instance for Person. + +If this requirement is not satisfied we get into anti-patterns like an `implicit ExecutionContext`, where changing our imports or moving a block of code can change which thread pool we run on. + +By definition, contextual values like this never satisfy this coherence requirement because there are lots of different `CorrelationId` and `UserId` values in our program. We need to pass them around explicitly or implicitly precisely because there are different ones. + +So while there are some cases where there might be different alternatives we want to consider this one I think we can rule out. + +### Solution 3 and 4: Environment and FiberRefs + +The final two alternatives are modeling these contextual values as part of the ZIO Environment or as `FiberRef` values. + +If we model both of these requirements as part of the environment our method signature would look like this: + +```scala +def someEffect(...): ZIO[UserId & CorrelationId, ErrorType, A] = + ??? +``` + +If we model them as `FiberRef` values we would define `FiberRef` values that described both the `UserId` and the `CorrelationId`: + +```scala +val currentUserId: FiberRef[UserId] = ??? +val currentCorrelationId: FiberRef[CorrelationId] = ??? + +def someEffect(...): ZIO[Any, ErrorType, A] = + ??? +``` + +Both of these approaches are similar in that they allow us to avoid the boilerplate associated with passing around the `UserId` and the `CorrelationId`, they allow us to treat `someEffect` as a value, and they allow us to locally modify the current value of the `UserId` and `CorrelationId`. + +The main difference between these two approaches is that with the ZIO Environment we reflect the fact that `someEffect` needs a `CorrelationId` and a `UserId` in the type signature, whereas when we use a `FiberRef` this requirement is not reflected in the type signature. + +Including these requirements in our type signature can be both an advantage and a disadvantage. The advantage is that we make explicit that `someEffect` requires this contextual information, and we cannot even run `someEffect` without providing it. The disadvantage is that we have to include these requirements in the type signatures of all of our workflows, which can "bubble up" through many method signatures and arguably exposes an implementation detail. + +So the question I would ask here, going back to our original answer, is whether including these requirements in your type signature is helpful for you to reason about your program. Some related questions you might ask yourself are "Would it make sense to run the workflow if a requirement were not provided?" and "Is there a sensible default value of this requirement"? + +Applying these to the CorrelationId and UserId we would be tempted to not include the `CorrelationId` in the environment. It may depend what we are doing with it but it seems like the `CorrelationId` is a low level implementation detail associated with logging that we do not need cluttering up our method signatures. There seems to be a very sensible default `CorrelationId` of None indicating that there is no `CorrelationId` associated with whatever we are doing and we can still run our program without having a `CorrelationId`, our logs will just not be as helpful as they otherwise would be which we can see and correct. + +On the other hand for `UserId` we could easily see coming to the opposite conclusion. If `UserId` is supposed to tell us which user we are supposed to look up in a database or whether we are supposed to be able to look up certain information at all then we may not even be able to run `someEffect` without having a `UserId`. Of course we could just fail at runtime but failing at runtime is much more severe than just logging less precisely and normally we want to use the type system to convert runtime failures to compile time failures. So this seems like it might be a great case for using the ZIO environment. + ## In ZIO ecosystem, there are lots of data types which they have `Z` prefix in their names. What this prefix stands for? Does it mean, that data type is effectual? No, it doesn't denote that the data type is effectual. Instead, the `Z` prefix is used for two purposes: