Building Pipelines
Effect pipelines allow for the composition and sequencing of operations on values, enabling the transformation and manipulation of data in a concise and modular manner.
Pipelines are an excellent way to structure your application and handle data transformations in a concise and modular manner. They offer several benefits:
-
Readability: Pipelines allow you to compose functions in a readable and sequential manner. You can clearly see the flow of data and the operations applied to it, making it easier to understand and maintain the code.
-
Code Organization: With pipelines, you can break down complex operations into smaller, manageable functions. Each function performs a specific task, making your code more modular and easier to reason about.
-
Reusability: Pipelines promote the reuse of functions. By breaking down operations into smaller functions, you can reuse them in different pipelines or contexts, improving code reuse and reducing duplication.
-
Type Safety: By leveraging the type system, pipelines help catch errors at compile-time. Functions in a pipeline have well-defined input and output types, ensuring that the data flows correctly through the pipeline and minimizing runtime errors.
The use of functions in the Effect ecosystem libraries is important for achieving tree shakeability and ensuring extensibility. Functions enable efficient bundling by eliminating unused code, and they provide a flexible and modular approach to extending the libraries’ functionality.
Tree shakeability refers to the ability of a build system to eliminate unused code during the bundling process. Functions are tree shakeable, while methods are not.
When functions are used in the Effect ecosystem, only the functions that are actually imported and used in your application will be included in the final bundled code. Unused functions are automatically removed, resulting in a smaller bundle size and improved performance.
On the other hand, methods are attached to objects or prototypes, and they cannot be easily tree shaken. Even if you only use a subset of methods, all methods associated with an object or prototype will be included in the bundle, leading to unnecessary code bloat.
Another important advantage of using functions in the Effect ecosystem is the ease of extensibility. With methods, extending the functionality of an existing API often requires modifying the prototype of the object, which can be complex and error-prone.
In contrast, with functions, extending the functionality is much simpler. You can define your own “extension methods” as plain old functions without the need to modify the prototypes of objects. This promotes cleaner and more modular code, and it also allows for better compatibility with other libraries and modules.
The pipe
function is a utility that allows us to compose functions in a readable and sequential manner. It takes the output of one function and passes it as the input to the next function in the pipeline. This enables us to build complex transformations by chaining multiple functions together.
Syntax
In this syntax, input
is the initial value, and func1
, func2
, …, funcN
are the functions to be applied in sequence. The result of each function becomes the input for the next function, and the final result is returned.
Here’s an illustration of how pipe
works:
It’s important to note that functions passed to pipe
must have a single argument because they are only called with a single argument.
Let’s see an example to better understand how pipe
works:
Example (Chaining Arithmetic Operations)
In the above example, we start with an input value of 5
. The increment
function adds 1
to the initial value, resulting in 6
. Then, the double
function doubles the value, giving us 12
. Finally, the subtractTen
function subtracts 10
from 12
, resulting in the final output of 2
.
The result is equivalent to subtractTen(double(increment(5)))
, but using pipe
makes the code more readable because the operations are sequenced from left to right, rather than nesting them inside out.
The Effect.map
function is used to transform the value inside an effect.
It takes a function and applies it to the value contained within the effect, creating a new effect with the transformed value.
Syntax
In the code above, transformation
is the function applied to the value, and myEffect
is the effect being transformed.
Example (Adding a Service Charge)
Here’s a practical example where we apply a service charge to a transaction amount:
The Effect.as
function is used to replace the original value inside an effect with a constant value.
Example (Replacing a Value)
The Effect.flatMap
function is used when you need to chain transformations that produce Effect
instances.
This is useful for asynchronous operations or computations that depend on the results of previous effects.
This function allows you to sequence multiple effects, ensuring that each step results in a new Effect
, “flattening” any nested structures that may arise.
Syntax
In the code above, transformation
is the function that takes a value and returns an Effect
, and myEffect
is the initial Effect
being transformed.
Example (Applying a Discount)
Make sure that all effects within Effect.flatMap
contribute to the final computation. If you ignore an effect, it can lead to unexpected behavior:
In this case, the Effect.sync
call is ignored and does not affect the result of applyDiscount(amount, 5)
. To handle effects correctly, make sure to explicitly chain them using functions like Effect.map
, Effect.flatMap
, Effect.andThen
, or Effect.tap
.
While developers may recognize flatMap
from its use in arrays, in the Effect
framework, it helps manage and flatten nested Effect
structures. For instance, if you’re working with an effect that contains nested arrays (Effect<Array<Array<A>>>
), you can flatten them like this:
Alternatively, you can use JavaScript’s built-in Array.prototype.flat()
method for array flattening.
The Effect.andThen
function is a convenient tool for sequencing actions in Effect
, combining both transformation scenarios handled by Effect.map
and Effect.flatMap
. It allows you to perform two actions, typically effects, where the second action may depend on the result of the first.
Syntax
The anotherEffect
action can take many forms:
- A value (similar to
Effect.as
) - A function returning a value (similar to
Effect.map
) - A
Promise
- A function returning a
Promise
- An
Effect
- A function returning an
Effect
(similar toEffect.flatMap
)
Example (Using andThen
to Sequence Actions)
Let’s look at an example comparing Effect.andThen
with Effect.map
and Effect.flatMap
:
Both Option and Either are commonly used for handling optional or missing values or simple error cases. These types integrate well with Effect.andThen
. When used with Effect.andThen
, the operations are categorized as scenarios 5 and 6 (as discussed earlier) because both Option
and Either
are treated as effects in this context.
Example (with Option)
You might expect the type of program
to be Effect<Option<number>, UnknownException, never>
, but it is actually Effect<number, UnknownException | NoSuchElementException, never>
.
This is because Option<A>
is treated as an effect of type Effect<A, NoSuchElementException>
, and as a result, the possible errors are combined into a union type.
Example (with Either)
Although one might expect the type of program
to be Effect<Either<number, string>, UnknownException, never>
, it is actually Effect<number, string | UnknownException, never>
.
This is because Either<A, E>
is treated as an effect of type Effect<A, E>
, meaning the errors are combined into a union type.
The Effect.tap
function works similarly to Effect.flatMap
, but with a key difference: the successful result of the function passed to tap
is ignored. This means the value from the previous computation remains available for the next step in the chain.
Example (Logging a Discount Application)
In this example, Effect.tap
is used to log the transaction amount before applying the discount, without modifying the value itself. The original value (amount
) remains available for the next operation (applyDiscount
).
Using Effect.tap
allows us to execute side effects during the computation without altering the result.
This can be useful for logging, performing additional actions, or observing the intermediate values without interfering with the main computation flow.
The Effect.all
function lets you combine multiple effects into a single effect. This new effect will produce a tuple containing the results of all the individual effects.
Syntax
By default the Effect.all
function executes all the provided effects sequentially (to explore options for
managing concurrency and controlling how these effects are executed, you can
refer to the Concurrency Options
documentation).
Each effect runs in order, and the function returns a new effect that contains the results of the original effects as a tuple.
The results in the tuple follow the same order as the effects passed to Effect.all
.
Example (Combining Configuration and Database Checks)
Let’s now combine the pipe
function, Effect.all
, and Effect.andThen
to create a pipeline that performs a sequence of transformations.
Example (Building a Transaction Pipeline)
This pipeline demonstrates how you can structure your code by combining different effects into a clear, readable flow.
Effect provides a pipe
method that works similarly to the pipe
method found in rxjs. This method allows you to chain multiple operations together, making your code more concise and readable.
Syntax
This is equivalent to using the pipe
function like this:
The pipe
method is available on all effects and many other data types, eliminating the need to import the pipe
function and saving you some keystrokes.
Example (Using the pipe
Method)
Let’s rewrite an earlier example, this time using the pipe
method.
Let’s summarize the transformation functions we have seen so far:
API | Input | Output |
---|---|---|
map | Effect<A, E, R> , A => B | Effect<B, E, R> |
flatMap | Effect<A, E, R> , A => Effect<B, E, R> | Effect<B, E, R> |
andThen | Effect<A, E, R> , * | Effect<B, E, R> |
tap | Effect<A, E, R> , A => Effect<B, E, R> | Effect<A, E, R> |
all | [Effect<A, E, R>, Effect<B, E, R>, ...] | Effect<[A, B, ...], E, R> |