Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions docs/about/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
143 changes: 143 additions & 0 deletions docs/reference/contextual/zio-environment-use-cases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
---
id: zio-environment-use-cases
title: "ZIO Environment 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, e.g. scopes and transactions
2. Business Logic, e.g. services and repositories

Let's discuss each of these in turn.

## 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
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
}
}

object Main extends ZIOAppDefault {
val run =
ZIO
.serviceWithZIO[ApplicationService](_.run)
.provide(
ApplicationService.live,
HighLevelService.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
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).
1 change: 1 addition & 0 deletions website/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ module.exports = {
items:
[
"reference/contextual/zenvironment",
"reference/contextual/zio-environment-use-cases",
{
type: "category",
label: "ZIO Layers",
Expand Down