Skip to content

Commit

Permalink
Prose edit of "Composability" #bruce #time 1h
Browse files Browse the repository at this point in the history
  • Loading branch information
Bruce Eckel committed Jul 23, 2024
1 parent 5634d99 commit 30a67e3
Showing 1 changed file with 70 additions and 77 deletions.
147 changes: 70 additions & 77 deletions Chapters/07_Composability.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ Issues that complicate composition include:
- Either-ness
- Environmental requirements

These concepts and their competing solutions will be expanded on and contrasted with ZIO throughout this chapter.
These concepts and their competing solutions will be expanded on and contrasted throughout this chapter.

We will utilize several pre-defined functions to highlight less-complete Effect alternatives.
The implementations are deliberately hidden to highlight the surprising nature of executing Effects and maintain focus on composability.
In this chapter, we use several pre-defined functions.
The implementations are deliberately hidden to highlight the surprising nature of executing Effects and to maintain focus on composability.

## Universal Composability

Expand Down Expand Up @@ -82,13 +82,11 @@ def diskFull =
ZLayer.empty
```

ZIOs compose in a way that covers all of these concerns.
Effects compose in a way that covers all of these concerns.
The methods for composability depend on the desired behavior.

When writing complex applications
, you will encounter APIs that that return limited data types.

ZIO provides conversion methods that take these limited data types and turn them into its single, universally composable type.
When writing complex applications, you will encounter APIs that return limited data types.
ZIO provides methods that convert these limited data types into a single, universally composable type.

```scala 3 mdoc:invisible
import zio.*
Expand Down Expand Up @@ -165,12 +163,12 @@ import scala.concurrent.Future

The original asynchronous datatype in Scala has several undesirable characteristics:

- Start executing immediately
- It starts executing immediately
- Cleanup is not guaranteed
- Must all fail with Exception
- Needs `ExecutionContext`s passed everywhere
- It fails with Exceptions
- It needs `ExecutionContext`s to be constantly passed

There is a function that returns a Future:
`getHeadLine` is one of our hidden-implementation functions that returns a Future:

```scala 3 mdoc:compile-only
import zio.*
Expand All @@ -179,12 +177,12 @@ import zio.direct.*
val future: Future[String] = getHeadLine()
```

By wrapping this in `ZIO.from`, it will:
By wrapping this in `ZIO.from`, we can:

- Defer execution
- Let us attach finalizer behavior
- Let us customize the failure type
- get the `ExecutionContext` it needs
- Attach finalizer behavior
- Customize the failure type
- Get the required `ExecutionContext`

```scala 3 mdoc:silent
import zio.*
Expand All @@ -208,7 +206,7 @@ def run =
getHeadlineZ()
```

Now let's confirm the behavior when the headline is not available.
What happens when the headline is not available?

```scala 3 mdoc:runzio
import zio.*
Expand All @@ -222,12 +220,9 @@ def run =

## Option

`Option` is the simplest of the alternate types you will encounter.
`Option` indicates that a value might not be available.
It does not deal with asynchronicity, failure types, or anything else.
It merely indicates that a value might not be available.

- Execution is not deferred
- Cannot interrupt the code that is producing these values
Execution is not deferred, and it cannot interrupt the code producing the `Option` values.

```scala 3 mdoc:silent
import zio.*
Expand All @@ -238,8 +233,8 @@ val result: Option[String] =
"content"
```

If you want to treat the case of a missing value as a failure, you can again use `ZIO.from`:
ZIO will convert `None` into a generic failure, giving you the opportunity to define a more specific type.
To treat the case of a missing value as a failure, use `ZIO.from`.
This converts `None` into a generic failure, giving you the opportunity to define a more specific type.

```scala 3 mdoc:silent
import zio.*
Expand Down Expand Up @@ -274,10 +269,11 @@ def run =

## Either

- Execution is not deferred
- Cannot interrupt the code that is producing these values
`Either` can hold objects of two different types.
Like `Option`, execution is not deferred, and `Either` cannot interrupt the code producing the `Either` values.

We have an existing function `wikiArticle` that checks for articles on a topic:
`wikiArticle` is the second implementation-hidden function.
It checks for articles on a topic:

```scala 3 mdoc:compile-only
import zio.*
Expand Down Expand Up @@ -319,8 +315,8 @@ def run =

## AutoCloseable

Java/Scala provide the `AutoCloseable` interface for defining finalizer behavior on objects.
While this is a big improvement over manually managing this in ad-hoc ways, the static scoping of this mechanism makes it clunky to use.
Java & Scala provide the `AutoCloseable` interface for defining finalizer behavior for objects.
This is an improvement over manual management, but its static scoping makes it clunky to use.

```scala 3 mdoc:invisible
import zio.*
Expand Down Expand Up @@ -407,7 +403,8 @@ def openFile(path: String) =
Try(entry)
```

We have an existing function that produces an `AutoCloseable`.
`openFile` is the third implementation-hidden function.
It produces an `AutoCloseable`:

```scala 3 mdoc:compile-only
import zio.*
Expand All @@ -416,8 +413,8 @@ import zio.direct.*
val file: AutoCloseable = openFile("file1")
```

Since `AutoCloseable` is a trait that can be implemented by arbitrary classes, we can't rely on `ZIO.from` to automatically manage this conversion for us.
In this situation, we should use the explicit `ZIO.fromAutoCloseable` function.
Since `AutoCloseable` is a trait that can be implemented by arbitrary classes, we can't rely on `ZIO.from` to automatically manage this conversion.
Instead, we use `ZIO.fromAutoCloseable`:

```scala 3 mdoc:silent
import zio.*
Expand All @@ -429,10 +426,10 @@ def openFileZ(path: String) =
openFile(path)
```

Once we do this, the `ZIO` runtime will manage the lifecycle of this object via the `Scope` mechanism.
Now the `ZIO` runtime manages the lifecycle of this object via the `Scope` mechanism.
For a more thorough discussion of this, see the [ZIO documentation](https://effectorientedprogramming.com/resources/zio/docs).

Now we open a `File`, and check if it contains a topic of interest.
Let's open a `File` and see if it contains a topic of interest:

```scala 3 mdoc:runzio
import zio.*
Expand All @@ -445,7 +442,7 @@ def run =
"topicOfInterest"
```

Now we highlight the difference between the static scoping of `Using` or `ZIO.fromAutoCloseable`.
We can highlight the difference between the static scoping of `Using` and `ZIO.fromAutoCloseable`:

```scala 3 mdoc:runzio
import zio.*
Expand All @@ -468,7 +465,8 @@ def run =
.ignore
```

With each new file we open, we have to nest our code deeper.
You can see that each new file means an additional level of code nesting.
Contrast this with:

```scala 3 mdoc:runzio
import zio.*
Expand All @@ -484,12 +482,13 @@ def run =
.run
```

Our code remains flat.
The Effect Oriented code remains flat.

## Try

Next we want to write to our `File`.
The existing API uses a `Try` to indicate success or failure.
Now let's write to a `File`.
`write` is the fourth implementation-hidden function.
It produces a `Try` to indicate success or failure:

```scala 3 mdoc:compile-only
import zio.*
Expand Down Expand Up @@ -526,10 +525,10 @@ def run =
.run
```

## Functions that throw
## Exception-Throwing Functions

We covered the deficiencies of throwing functions in the previous chapter, so we will not belabor the point here.
We still want to show how they can be converted to Effects and cleanly fit into our composability story.
We [previously covered](#Chapter-Failure) the deficiencies of exception-throwing functions.
Fortunately they can be converted to Effects, so they cleanly fit into our composability story.

```scala 3 mdoc:runzio
def run =
Expand Down Expand Up @@ -557,14 +556,14 @@ def summaryForZ(file: File, topic: String) =
NoSummaryAvailable(topic)
```

## Slow, blocking functions
## Slow, Blocking Functions

Most of our examples in this chapter have specific failure behaviors that we handle.
However, we must also consider functions that are too slow.
Up to a point, latency is just the normal cost of doing business, but eventually it becomes unacceptable.
Most examples in this chapter handle specific failure behaviors.
We must also consider functions that are too slow.
Latency can be considered a cost of doing business, but eventually it becomes unacceptable.

Here, we are using a local Large Language Model to summarize content.
It does not have the same failure modes as the other functions, but its performance varies wildly.
We will use a local Large Language Model (LLM), expressed through our implementation-hidden function `summarize`.
It does not have the same failure modes as the other functions, and its performance varies wildly.

```scala 3 mdoc:invisible
import zio.*
Expand Down Expand Up @@ -599,12 +598,12 @@ def run =
summarize("long article")
```

This function is blocking, although it is not obvious from the signature.
This brings several downsides:
`summarize` is a blocking function, which is not obvious from the signature.
This produces downsides:

- Too many concurrent blocking operations can prevent progress of other operations
- Very difficult to manage
- Blocking performance varies wildly between environments
- Too many concurrent blocking operations can prevent progress of other operations.
- Blocking functions are difficult to manage.
- Blocking performance varies wildly between environments.

```scala 3 mdoc:silent
import zio.*
Expand All @@ -621,35 +620,31 @@ def summarizeZ(article: String) =
4000.millis
```

Now we have a way to confine the impact that this function has on our application.
This confines the impact that `summarize` has on the application.

```scala 3 mdoc:runzio:liveclock
def run =
summarizeZ("long article")
```

Long-running invocations will be interrupted if they take too long.
Long-running calls are interrupted if they take too long:

```scala 3 mdoc:runzio:liveclock
def run =
summarizeZ("space")
```

However, `attemptBlockingInterrupt` comes with a performance cost.
Carefully consider the trade-offs between implementing an upper bound vs slowing down the average run when using this function.
`attemptBlockingInterrupt` comes with a performance cost, so you must consider the trade-off between implementing an upper bound and the performance impact.

## Losing your Composure
## Solving the Composition Problem

Each of the original approaches gives you benefits, but you can't easily assemble a program that utilizes all of them.
The non-Effect approaches have benefits, but you can't assemble a program that uses all of them.
They must be manually transformed into each other.

Instead of the best of all worlds, you get the pain of all worlds.
eg `Closeable[Future[Either[Throwable, A]]]`
Instead of the best of all worlds, you get the pain of all worlds, because it looks like this:
`Closeable[Future[Either[Throwable, A]]]`.
The ordering of the nesting is significant, confusing, and not easily changed.

## Fully Composed

Now that we have all of these well-defined Effects, we can wield them in any combination and sequence we desire.
With an Effect System, we can wield Effects in any combination and sequence we desire.

```scala 3 mdoc:silent
import zio.*
Expand Down Expand Up @@ -682,11 +677,10 @@ val researchHeadline =
summary
```

We consider this sequence the most significant achievement of this book.
Without a powerful, general Effect type, you are constantly struggling to jump from 1 limited Effect to the next.
With ZIO, you can build real-word, complex applications that all flow cleanly into one supreme type.
Look at how straightforward this code is.
You can now build complex real-world applications that flow cleanly, because you're using an Effect System.

We now step through all the possible scenarios that can occur in our application.
Let's step through all possible scenarios for our application.

### Headline Not Available

Expand All @@ -712,7 +706,7 @@ def run =
researchHeadline
```

### Exception when reading from file
### Exception when Reading from File

```scala 3 mdoc:runzio:liveclock
import zio.*
Expand Down Expand Up @@ -763,7 +757,7 @@ def run =

### Happy Path

And finally, we see the longest, successful pathway through our application:
Finally, we see the full successful pathway through our application:

```scala 3 mdoc:runzio:liveclock
import zio.*
Expand All @@ -777,12 +771,11 @@ def run =

## Effects are Values

We want to revisit and re-emphasize that Effects are values.
This can be difficult to remember when viewing and executing larger programs.
Representing a complex workflow as a value allows us to manipulate it in ways that are not possible with other approaches.
Representing a complex workflow as a value allows us to manipulate it in ways that are not possible with non-Effect approaches.

Suppose the requirements of the system change, and now you must ensure that the whole process completes within a strict time limit.
Even though we already have a narrow timeout attached to the AI summarize call, we are still free to attach a more restrictive timeout.
Suppose the requirements of the system change such that the whole process must complete within a strict time limit.
Although we have a narrow timeout attached to the AI summarize call, we are still free to attach a more restrictive timeout:

```scala 3 mdoc:silent
val strictResearch =
Expand All @@ -797,8 +790,8 @@ override val bootstrap = stockMarketHeadline
def run =
strictResearch
```
Repeating is a form of composability, because you are composing a program value with itself and a delay.
Now that we have a nice, single-shot workflow that will analyze the current headline, we can make it run every day.
Repeating is a form of composability, because you are composing a program value with itself along with a delay.
Now that we have a nice, single-shot workflow that analyzes the current headline, we can make it run every day:

```scala 3 mdoc:silent
val daily =
Expand Down

0 comments on commit 30a67e3

Please sign in to comment.