diff --git a/Chapters/07_Composability.md b/Chapters/07_Composability.md index b278c061..4226b63c 100644 --- a/Chapters/07_Composability.md +++ b/Chapters/07_Composability.md @@ -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 @@ -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.* @@ -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.* @@ -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.* @@ -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.* @@ -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.* @@ -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.* @@ -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.* @@ -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.* @@ -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.* @@ -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.* @@ -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.* @@ -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.* @@ -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.* @@ -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.* @@ -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 = @@ -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.* @@ -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.* @@ -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.* @@ -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 @@ -712,7 +706,7 @@ def run = researchHeadline ``` -### Exception when reading from file +### Exception when Reading from File ```scala 3 mdoc:runzio:liveclock import zio.* @@ -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.* @@ -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 = @@ -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 =