Simplifying Excessive Nesting
On this page
Suppose you want to create a custom function elapsed
that prints the elapsed time taken by an effect to execute.
Using plain pipe
Initially, you may come up with code that uses the standard pipe
method, but this approach can lead to excessive nesting and result in verbose and hard-to-read code:
ts
import {Effect ,Console } from "effect"// Get the current timestampconstnow =Effect .sync (() => newDate ().getTime ())// Prints the elapsed time occurred to `self` to executeconstelapsed = <R ,E ,A >(self :Effect .Effect <A ,E ,R >):Effect .Effect <A ,E ,R > =>now .pipe (Effect .flatMap ((startMillis ) =>self .pipe (Effect .flatMap ((result ) =>now .pipe (Effect .flatMap ((endMillis ) => {// Calculate the elapsed time in millisecondsconstelapsed =endMillis -startMillis // Log the elapsed timereturnConsole .log (`Elapsed: ${elapsed }`).pipe (Effect .map (() =>result ))}))))))// Simulates a successful computation with a delay of 200 millisecondsconsttask =Effect .succeed ("some task").pipe (Effect .delay ("200 millis"))constprogram =elapsed (task )Effect .runPromise (program ).then (console .log )/*Output:Elapsed: 204some task*/
ts
import {Effect ,Console } from "effect"// Get the current timestampconstnow =Effect .sync (() => newDate ().getTime ())// Prints the elapsed time occurred to `self` to executeconstelapsed = <R ,E ,A >(self :Effect .Effect <A ,E ,R >):Effect .Effect <A ,E ,R > =>now .pipe (Effect .flatMap ((startMillis ) =>self .pipe (Effect .flatMap ((result ) =>now .pipe (Effect .flatMap ((endMillis ) => {// Calculate the elapsed time in millisecondsconstelapsed =endMillis -startMillis // Log the elapsed timereturnConsole .log (`Elapsed: ${elapsed }`).pipe (Effect .map (() =>result ))}))))))// Simulates a successful computation with a delay of 200 millisecondsconsttask =Effect .succeed ("some task").pipe (Effect .delay ("200 millis"))constprogram =elapsed (task )Effect .runPromise (program ).then (console .log )/*Output:Elapsed: 204some task*/
To address this issue and make the code more manageable, there is a solution: the "do simulation."
Using the "do simulation"
The "do simulation" in Effect allows you to write code in a more declarative style, similar to the "do notation" in other programming languages. It provides a way to define variables and perform operations on them using functions like Effect.bind
and Effect.let
.
Here's how the do simulation works:
-
Start the do simulation using the
Effect.Do
value:tsconst program = Effect.Do.pipe(/* ... rest of the code */)tsconst program = Effect.Do.pipe(/* ... rest of the code */) -
Within the do simulation scope, you can use the
Effect.bind
function to define variables and bind them toEffect
values:tsEffect.bind("variableName", (scope) => effectValue)tsEffect.bind("variableName", (scope) => effectValue)
variableName
is the name you choose for the variable you want to define. It must be unique within the scope.effectValue
is theEffect
value that you want to bind to the variable. It can be the result of a function call or any other validEffect
value.
-
You can accumulate multiple
Effect.bind
statements to define multiple variables within the scope:tsEffect.bind("variable1", () => effectValue1),Effect.bind("variable2", ({ variable1 }) => effectValue2),// ... additional bind statementstsEffect.bind("variable1", () => effectValue1),Effect.bind("variable2", ({ variable1 }) => effectValue2),// ... additional bind statements -
Inside the do simulation scope, you can also use the
Effect.let
function to define variables and bind them to simple values:tsEffect.let("variableName", (scope) => simpleValue)tsEffect.let("variableName", (scope) => simpleValue)
variableName
is the name you give to the variable. Like before, it must be unique within the scope.simpleValue
is the value you want to assign to the variable. It can be a simple value like anumber
,string
, orboolean
.
-
Regular Effect functions like
Effect.flatMap
,Effect.tap
, andEffect.map
can still be used within the do simulation. These functions will receive the accumulated variables as arguments within the scope:tsEffect.flatMap(({ variable1, variable2 }) => {// Perform operations using variable1 and variable2// Return an `Effect` value as the result})tsEffect.flatMap(({ variable1, variable2 }) => {// Perform operations using variable1 and variable2// Return an `Effect` value as the result})
With the do simulation, you can rewrite the elapsed
combinator like this:
ts
import {Effect ,Console } from "effect"// Get the current timestampconstnow =Effect .sync (() => newDate ().getTime ())constelapsed = <R ,E ,A >(self :Effect .Effect <A ,E ,R >):Effect .Effect <A ,E ,R > =>Effect .Do .pipe (Effect .bind ("startMillis", () =>now ),Effect .bind ("result", () =>self ),Effect .bind ("endMillis", () =>now ),Effect .let ("elapsed",({startMillis ,endMillis }) =>endMillis -startMillis // Calculate the elapsed time in milliseconds),Effect .tap (({elapsed }) =>Console .log (`Elapsed: ${elapsed }`)), // Log the elapsed timeEffect .map (({result }) =>result ))// Simulates a successful computation with a delay of 200 millisecondsconsttask =Effect .succeed ("some task").pipe (Effect .delay ("200 millis"))constprogram =elapsed (task )Effect .runPromise (program ).then (console .log )/*Output:Elapsed: 204some task*/
ts
import {Effect ,Console } from "effect"// Get the current timestampconstnow =Effect .sync (() => newDate ().getTime ())constelapsed = <R ,E ,A >(self :Effect .Effect <A ,E ,R >):Effect .Effect <A ,E ,R > =>Effect .Do .pipe (Effect .bind ("startMillis", () =>now ),Effect .bind ("result", () =>self ),Effect .bind ("endMillis", () =>now ),Effect .let ("elapsed",({startMillis ,endMillis }) =>endMillis -startMillis // Calculate the elapsed time in milliseconds),Effect .tap (({elapsed }) =>Console .log (`Elapsed: ${elapsed }`)), // Log the elapsed timeEffect .map (({result }) =>result ))// Simulates a successful computation with a delay of 200 millisecondsconsttask =Effect .succeed ("some task").pipe (Effect .delay ("200 millis"))constprogram =elapsed (task )Effect .runPromise (program ).then (console .log )/*Output:Elapsed: 204some task*/
In this solution, we use the do simulation to simplify the code. The elapsed
function now starts with Effect.Do
to enter the simulation scope.
Inside the scope, we use Effect.bind
to define variables and bind them to the corresponding effects.
Effect.gen
Using The most concise and convenient solution is to use the Effect.gen
constructor, which allows you to work with generators when dealing with effects. This approach leverages the native scope provided by the generator syntax, avoiding excessive nesting and leading to more concise code.
ts
import {Effect } from "effect"// Get the current timestampconstnow =Effect .sync (() => newDate ().getTime ())// Prints the elapsed time occurred to `self` to executeconstelapsed = <R ,E ,A >(self :Effect .Effect <A ,E ,R >):Effect .Effect <A ,E ,R > =>Effect .gen (function* (_ ) {conststartMillis = yield*_ (now )constresult = yield*_ (self )constendMillis = yield*_ (now )// Calculate the elapsed time in millisecondsconstelapsed =endMillis -startMillis // Log the elapsed timeconsole .log (`Elapsed: ${elapsed }`)returnresult })// Simulates a successful computation with a delay of 200 millisecondsconsttask =Effect .succeed ("some task").pipe (Effect .delay ("200 millis"))constprogram =elapsed (task )Effect .runPromise (program ).then (console .log )/*Output:Elapsed: 204some task*/
ts
import {Effect } from "effect"// Get the current timestampconstnow =Effect .sync (() => newDate ().getTime ())// Prints the elapsed time occurred to `self` to executeconstelapsed = <R ,E ,A >(self :Effect .Effect <A ,E ,R >):Effect .Effect <A ,E ,R > =>Effect .gen (function* (_ ) {conststartMillis = yield*_ (now )constresult = yield*_ (self )constendMillis = yield*_ (now )// Calculate the elapsed time in millisecondsconstelapsed =endMillis -startMillis // Log the elapsed timeconsole .log (`Elapsed: ${elapsed }`)returnresult })// Simulates a successful computation with a delay of 200 millisecondsconsttask =Effect .succeed ("some task").pipe (Effect .delay ("200 millis"))constprogram =elapsed (task )Effect .runPromise (program ).then (console .log )/*Output:Elapsed: 204some task*/
In this solution, we switch to using generators to simplify the code. The elapsed
function now uses a generator function (Effect.gen
) to define the flow of execution. Within the generator, we use yield*
to invoke effects and bind their results to variables. This eliminates the nesting and provides a more readable and sequential code structure.
The generator style in Effect uses a more linear and sequential flow of execution, resembling traditional imperative programming languages. This makes the code easier to read and understand, especially for developers who are more familiar with imperative programming paradigms.
On the other hand, the pipe style can lead to excessive nesting, especially when dealing with complex effectful computations. This can make the code harder to follow and debug.
For more information on how to use generators in Effect, you can refer to the Using Generators in Effect guide.