Practical understanding of some common fp concepts will allow you to read Effect programs and the Effect codebase. Combined, they are a powerful tool to write clear, concise, maintainable code.
Module methods: Array.forEach(array, f) instead of array.forEach(f)
Array.map(array, (n: number) => n + 1) returns number[]
Both above concepts enable pipe, which passes the previous result as the first argument of the next function: pipe([1, 2, 3], Array.map(plusOne), Array.reduce(sum), console.log) will log 9.
map and flatMap are general fp concepts, useful beyond arrays:
map: transforms contained value: Effect.map(Effect.success(1), n => n + 1) => Effect<number> that will yield 2
flatMap: transforms the whole container, based on the contained value: Effect.flatMap(Effect.success(1), n => Effect.succeed(2)) => also Effect<number> that will yield 2. Use this when the transformation involves an Effect (i.e. for any async work, or work that may fail).
Either<A, B>: represents values that may be of two types, which is a very common occurrence. You may see it to represent success/failure scenarios.
Option<T>: represents a value that may or may not be. The Option module provides many useful methods.
In my introduction I mentioned that in some ways Effect feels like a new language. This is especially true if you don’t have experience with functional programming paradigms. My goal in this post is to cover some key concepts commonly present in Effect codebases and that constitute a powerful tool for writing terse yet maintainable code. This is not a functional programming class. The approach is primarily practical.
Module methods
You may be used to working with class instances, which expose methods to interact with their internal state. For example, [1, 2, 3].forEach(console.log): You access forEach in the array itself. The subject of the operation is implicit (in the forEach implementation, this is the array).
This remains true as long as you are working with JavaScript classes. There are JavaScript classes in the Effect library, and classes are sometimes the best tool for the job. However, most of the Effect interfaces are geared towards exposing methods that don’t have an implicit subject: i.e. they don’t hold their own state. Instead, module methods take that subject as their first argument.
Since we are all familiar with array operations, we can take as an example the Array module that Effect exposes as an alternative way to interact with arrays:
The console module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
A Console class with methods such as console.log(), console.error() andconsole.warn() that can be used to write to any Node.js stream.
A global console instance configured to write to process.stdout and process.stderr. The global console can be used without callingrequire('console').
Warning: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the note on process I/O for
more information.
Example using the global console:
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(newError('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
constname='Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
Example using the Console class:
constout=getStreamSomehow();
consterr=getStreamSomehow();
constmyConsole=new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(newError('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
Prints to stdout with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to printf(3) (the arguments are all passed to util.format()).
constcount=5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
See util.format() for more information.
@since ― v0.1.100
log(
n: number
n)
8
);
Similarly, when you interact with Effect, you do it via the methods under the Effect namespace (e.g. Effect.provide(effect, layer), Effect.runPromise(effect)). Again, note how their first argument is an Effect.
Another example is the methods under the Schema namespace, such as Schema.decode, Schema.optional, also taking a Schema as their first argument.
pipe
The concept of “pipes” as a utility to pass data across processes or functions has become almost mainstream in the last few years. Some languages have had it for ages. Unix has the | operator to pass data between processes. Languages like Haskell, F#, Elixir, Julia and Elm (|>) use it for function application.
In JavaScript, there is a long list of libraries that have supported and encouraged some sort of pipe (lodash flow, Ramda and RxJS pipe), and there is a proposal to adopt them into the language.
While the operator doesn’t work exactly the same way in all languages and libraries, the core idea of taking something from the left side and applying it to the right side is universal. Pipe operators promote better organization of code, reusability, and readability. Wise usage of the pipe operator often yields code that conveys the flow of data at a glance. That said, it can take a while for your brain to get used to seeing that data flow without having to think, if you are not used to pipes.
pipe in Effect passes the result of the left-hand operation as the first argument of the right-hand operation.
So pipe(x, f) is equivalent to f(x). Of course, this is not useful with a single argument. Perhaps an example like the following illustrates it better:
1
import {
functionpipe<A>(a:A):A (+19overloads)
Pipes the value of an expression into a pipeline of functions.
Details
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.
import { pipe } from"effect"
constresult=pipe(input, func1, func2, ..., funcN)
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.
Pipes the value of an expression into a pipeline of functions.
Details
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.
import { pipe } from"effect"
constresult=pipe(input, func1, func2, ..., funcN)
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.
The console module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
A Console class with methods such as console.log(), console.error() andconsole.warn() that can be used to write to any Node.js stream.
A global console instance configured to write to process.stdout and process.stderr. The global console can be used without callingrequire('console').
Warning: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the note on process I/O for
more information.
Example using the global console:
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(newError('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
constname='Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
Example using the Console class:
constout=getStreamSomehow();
consterr=getStreamSomehow();
constmyConsole=new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(newError('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
Prints to stdout with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to printf(3) (the arguments are all passed to util.format()).
constcount=5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
See util.format() for more information.
@since ― v0.1.100
log(
constinvoice:string
invoice) // "97.20 €"
Consider the alternatives of a complex nesting of function calls, or the creation of variables to hold intermediate values.
Tip
Not all code needs to be written this way. Imperative code can still be more clear or practical. Some teams lean into pipe when clearly there is some data being transformed along several steps, or to provision dependencies, but fall back to imperative code for most application logic.
Here are some common operations you’ll see piped. For now you don’t need to understand each of the operations. We’ll cover them later.
Effect.map, Effect.flatMap to transform values inside an Effect
Effect.match to transform the result and the error channels in an Effect
Effect.provide and Effect.provideService to provide dependencies (requirements) to an Effect
Layer.provide and Layer.provideMerge to combine dependency layers
Many Schema methods like Schema.decode, Schema.maxLength, Schema.optional, Schema.mutable…
Here’s another example of a server handling a client request:
1
import {
import Data
Data,
import Effect
@since ― 2.0.0
@since ― 2.0.0
@since ― 2.0.0
Effect,
functionpipe<A>(a:A):A (+19overloads)
Pipes the value of an expression into a pipeline of functions.
Details
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.
import { pipe } from"effect"
constresult=pipe(input, func1, func2, ..., funcN)
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.
The Effect interface defines a value that describes a workflow or job,
which can succeed or fail.
Details
The Effect interface represents a computation that can model a workflow
involving various types of operations, such as synchronous, asynchronous,
concurrent, and parallel interactions. It operates within a context of type
R, and the result can either be a success with a value of type A or a
failure with an error of type E. The Effect is designed to handle complex
interactions with external resources, offering advanced features such as
fiber-based concurrency, scheduling, interruption handling, and scalability.
This makes it suitable for tasks that require fine-grained control over
concurrency and error management.
To execute an Effect value, you need a Runtime, which provides the
environment necessary to run and manage the computation.
Pipes the value of an expression into a pipeline of functions.
Details
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.
import { pipe } from"effect"
constresult=pipe(input, func1, func2, ..., funcN)
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.
The ParseJson combinator provides a method to convert JSON strings into the unknown type using the underlying
functionality of JSON.parse. It also utilizes JSON.stringify for encoding.
You can optionally provide a ParseJsonOptions to configure both JSON.parse and JSON.stringify executions.
Optionally, you can pass a schema Schema<A, I, R> to obtain an A type instead of unknown.
Use andThen when you need to run multiple actions in sequence, with the
second action depending on the result of the first. This is useful for
combining effects or handling computations that must happen in order.
Details
The second action can be:
A constant value (similar to
as
)
A function returning a value (similar to
map
)
A Promise
A function returning a Promise
An Effect
A function returning an Effect (similar to
flatMap
)
Note:andThen works well with both Option and Either types,
treating them as effects.
Example (Applying a Discount Based on Fetched Amount)
import { pipe, Effect } from"effect"
// Function to apply a discount safely to a transaction amount
constapplyDiscount= (
total:number,
discountRate:number
):Effect.Effect<number, Error> =>
discountRate ===0
? Effect.fail(newError("Discount rate cannot be zero"))
Transforms or modifies the error produced by an effect without affecting its
success value.
When to Use
This function is helpful when you want to enhance the error with additional
information, change the error type, or apply custom error handling while
keeping the original behavior of the effect's success values intact. It only
operates on the error channel and leaves the success channel unchanged.
map takes a function and applies it to the value contained within an
effect, creating a new effect with the transformed value.
It's important to note that effects are immutable, meaning that the original
effect is not modified. Instead, a new effect is returned with the updated
value.
map takes a function and applies it to the value contained within an
effect, creating a new effect with the transformed value.
It's important to note that effects are immutable, meaning that the original
effect is not modified. Instead, a new effect is returned with the updated
value.
@see ― mapError for a version that operates on the error channel.
@see ― mapBoth for a version that operates on both channels.
@see ― flatMap or andThen for a version that can return a new effect.
@since ― 2.0.0
map(
consttimesTwo: (n:number) =>number
timesTwo)
9
)
Dual functions
Because JavaScript doesn’t support currying natively, you can’t magically take any function and elide its first argument to conveniently use it in your pipe. In other words, the following is not possible:
1
import {
functionpipe<A>(a:A):A (+19overloads)
Pipes the value of an expression into a pipeline of functions.
Details
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.
import { pipe } from"effect"
constresult=pipe(input, func1, func2, ..., funcN)
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.
pipe<number, never>(a: number, ab: (a:number) => never): never (+19 overloads)
Pipes the value of an expression into a pipeline of functions.
Details
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.
import { pipe } from"effect"
constresult=pipe(input, func1, func2, ..., funcN)
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.
However, because TypeScript supports function overloads, it is possible to do better and support both signatures at the same time, under the divide name. This is indeed what Effect does with many of the methods it exposes. This is what makes Array.map, Effect.map, Effect.provide and others suitable for both direct usage and usage inside a pipe.
Note
While many common Effect methods are dual, you will not typically have to implement dual functions yourself for application code. However, if you do, check out Function.dual, which makes it easy to do this in a consistent way, for example to implement a divide function that support both divide(n1, n2) and divide(n2)(n1).
map
map transforms an inner value into another inner value. It’s commonplace now in many languages to transform arrays. In JavaScript, it’s not surprising that [1, 2, 3].map(n => n + 1) yields [2, 3, 4]. But the concept doesn’t only apply to arrays. It can also apply to other types of value containers, including those that have only a single inner value, such as an Effect or a Stream.
It might make more sense if you think about map from a type perspective. You can think of Array.map as one that transforms Array<I> via a function (I) => O into Array<O>:
function (typeparameter) Iin <I, O>(container:Array<I>, operation: (inner:I) =>O):Array<O>
I,
function (typeparameter) Oin <I, O>(container:Array<I>, operation: (inner:I) =>O):Array<O>
O>(
container: I[]
container:
interfaceArray<T>
Array<
function (typeparameter) Iin <I, O>(container:Array<I>, operation: (inner:I) =>O):Array<O>
I>,
operation: (inner:I) =>O
operation: (
inner: I
inner:
function (typeparameter) Iin <I, O>(container:Array<I>, operation: (inner:I) =>O):Array<O>
I) =>
function (typeparameter) Oin <I, O>(container:Array<I>, operation: (inner:I) =>O):Array<O>
O) =>
interfaceArray<T>
Array<
function (typeparameter) Oin <I, O>(container:Array<I>, operation: (inner:I) =>O):Array<O>
O>
In the section above about module methods, Array.forEach was used as an example of what Effect exposes under its Array module. But Array.map also exists:
Array.map follows the module method convention, with the subject (the array), being its first argument. Array.map is also a dual function, so you can naturally use it in a pipe:
1
import {
import Array
Array,
functionpipe<A>(a:A):A (+19overloads)
Pipes the value of an expression into a pipeline of functions.
Details
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.
import { pipe } from"effect"
constresult=pipe(input, func1, func2, ..., funcN)
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.
Pipes the value of an expression into a pipeline of functions.
Details
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.
import { pipe } from"effect"
constresult=pipe(input, func1, func2, ..., funcN)
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.
Let’s put both signatures of Array.map together for clarity:
1
interface
interfaceArrayMap
ArrayMap {
2
<
function (typeparameter) Iin <I, O>(container:Array<I>, operation: (inner:I) =>O):Array<O>
I,
function (typeparameter) Oin <I, O>(container:Array<I>, operation: (inner:I) =>O):Array<O>
O>(
container: I[]
container:
interfaceArray<T>
Array<
function (typeparameter) Iin <I, O>(container:Array<I>, operation: (inner:I) =>O):Array<O>
I>,
operation: (inner:I) =>O
operation: (
inner: I
inner:
function (typeparameter) Iin <I, O>(container:Array<I>, operation: (inner:I) =>O):Array<O>
I) =>
function (typeparameter) Oin <I, O>(container:Array<I>, operation: (inner:I) =>O):Array<O>
O):
interfaceArray<T>
Array<
function (typeparameter) Oin <I, O>(container:Array<I>, operation: (inner:I) =>O):Array<O>
O>;
3
<
function (typeparameter) Iin <I, O>(operation: (inner:I) =>O): (container:Array<I>) =>Array<O>
I,
function (typeparameter) Oin <I, O>(operation: (inner:I) =>O): (container:Array<I>) =>Array<O>
O>(
operation: (inner:I) =>O
operation: (
inner: I
inner:
function (typeparameter) Iin <I, O>(operation: (inner:I) =>O): (container:Array<I>) =>Array<O>
I) =>
function (typeparameter) Oin <I, O>(operation: (inner:I) =>O): (container:Array<I>) =>Array<O>
O): (
container: I[]
container:
interfaceArray<T>
Array<
function (typeparameter) Iin <I, O>(operation: (inner:I) =>O): (container:Array<I>) =>Array<O>
I>) =>
interfaceArray<T>
Array<
function (typeparameter) Oin <I, O>(operation: (inner:I) =>O): (container:Array<I>) =>Array<O>
O>;
4
}
Like I said, arrays are not the only thing you can map over. You can map over an Effect, or a Stream, for example. What would the signature of Effect.map look like?
1
importtype {
interfaceEffect<outA, outE=never, outR=never>
The Effect interface defines a value that describes a workflow or job,
which can succeed or fail.
Details
The Effect interface represents a computation that can model a workflow
involving various types of operations, such as synchronous, asynchronous,
concurrent, and parallel interactions. It operates within a context of type
R, and the result can either be a success with a value of type A or a
failure with an error of type E. The Effect is designed to handle complex
interactions with external resources, offering advanced features such as
fiber-based concurrency, scheduling, interruption handling, and scalability.
This makes it suitable for tasks that require fine-grained control over
concurrency and error management.
To execute an Effect value, you need a Runtime, which provides the
environment necessary to run and manage the computation.
function (typeparameter) Iin <I, O>(container:Effect<I>, operation: (inner:I) =>O):Effect<O>
I,
function (typeparameter) Oin <I, O>(container:Effect<I>, operation: (inner:I) =>O):Effect<O>
O>(
container: Effect<I, never, never>
container:
interfaceEffect<outA, outE=never, outR=never>
The Effect interface defines a value that describes a workflow or job,
which can succeed or fail.
Details
The Effect interface represents a computation that can model a workflow
involving various types of operations, such as synchronous, asynchronous,
concurrent, and parallel interactions. It operates within a context of type
R, and the result can either be a success with a value of type A or a
failure with an error of type E. The Effect is designed to handle complex
interactions with external resources, offering advanced features such as
fiber-based concurrency, scheduling, interruption handling, and scalability.
This makes it suitable for tasks that require fine-grained control over
concurrency and error management.
To execute an Effect value, you need a Runtime, which provides the
environment necessary to run and manage the computation.
@since ― 2.0.0
@since ― 2.0.0
Effect<
function (typeparameter) Iin <I, O>(container:Effect<I>, operation: (inner:I) =>O):Effect<O>
I>,
operation: (inner:I) =>O
operation: (
inner: I
inner:
function (typeparameter) Iin <I, O>(container:Effect<I>, operation: (inner:I) =>O):Effect<O>
I) =>
function (typeparameter) Oin <I, O>(container:Effect<I>, operation: (inner:I) =>O):Effect<O>
O) =>
interfaceEffect<outA, outE=never, outR=never>
The Effect interface defines a value that describes a workflow or job,
which can succeed or fail.
Details
The Effect interface represents a computation that can model a workflow
involving various types of operations, such as synchronous, asynchronous,
concurrent, and parallel interactions. It operates within a context of type
R, and the result can either be a success with a value of type A or a
failure with an error of type E. The Effect is designed to handle complex
interactions with external resources, offering advanced features such as
fiber-based concurrency, scheduling, interruption handling, and scalability.
This makes it suitable for tasks that require fine-grained control over
concurrency and error management.
To execute an Effect value, you need a Runtime, which provides the
environment necessary to run and manage the computation.
@since ― 2.0.0
@since ― 2.0.0
Effect<
function (typeparameter) Oin <I, O>(container:Effect<I>, operation: (inner:I) =>O):Effect<O>
O>
With Effect.map, you transform the success value in the Effect. Let’s say you have an Effect<number>. With Effect.map, you can transform that number into another number, or another type entirely:
map takes a function and applies it to the value contained within an
effect, creating a new effect with the transformed value.
It's important to note that effects are immutable, meaning that the original
effect is not modified. Instead, a new effect is returned with the updated
value.
map takes a function and applies it to the value contained within an
effect, creating a new effect with the transformed value.
It's important to note that effects are immutable, meaning that the original
effect is not modified. Instead, a new effect is returned with the updated
value.
map takes a function and applies it to the value contained within an
effect, creating a new effect with the transformed value.
It's important to note that effects are immutable, meaning that the original
effect is not modified. Instead, a new effect is returned with the updated
value.
As an exercise, what do you think is the return type of Stream.map in this function?
1
Stream.map(Stream.succeed(1), (n) => n +1)
flatMap
A closely related concept is flatMap. It is different from map in that the return type of the callback is the same as the container, not the contained value.
For an array, the flatMap functor (the function that applies the transformation, the callback), must return an Array:
Applies a function to each element in an array and returns a new array containing the concatenated mapped elements.
@since ― 2.0.0
flatMap([1, 2, 3],
x: number
x=> [
x: number
x])
constresult: [number, ...number[]]
4
var console:Console
The console module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
A Console class with methods such as console.log(), console.error() andconsole.warn() that can be used to write to any Node.js stream.
A global console instance configured to write to process.stdout and process.stderr. The global console can be used without callingrequire('console').
Warning: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the note on process I/O for
more information.
Example using the global console:
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(newError('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
constname='Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
Example using the Console class:
constout=getStreamSomehow();
consterr=getStreamSomehow();
constmyConsole=new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(newError('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
Prints to stdout with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to printf(3) (the arguments are all passed to util.format()).
constcount=5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
See util.format() for more information.
@since ― v0.1.100
log(
constresult: [number, ...number[]]
result) // logs [1, 2, 3]
Back to Effect, we can replicate the earlier example where we added 1 to the success value in an Effect, but using Effect.flatMap. The functor will have to return an Effect instead of just a number:
flatMap lets you sequence effects so that the result of one effect can be
used in the next step. It is similar to flatMap used with arrays but works
specifically with Effect instances, allowing you to avoid deeply nested
effect structures.
Since effects are immutable, flatMap always returns a new effect instead of
changing the original one.
When to Use
Use flatMap when you need to chain multiple effects, ensuring that each
step produces a new Effect while flattening any nested effects that may
occur.
Example
import { pipe, Effect } from"effect"
// Function to apply a discount safely to a transaction amount
constapplyDiscount= (
total:number,
discountRate:number
):Effect.Effect<number, Error> =>
discountRate ===0
? Effect.fail(newError("Discount rate cannot be zero"))
Creates an Effect that always succeeds with a given value.
When to Use
Use this function when you need an effect that completes successfully with a
specific value without any errors or external dependencies.
Example (Creating a Successful Effect)
import { Effect } from"effect"
// Creating an effect that represents a successful scenario
//
// ┌─── Effect<number, never, never>
// ▼
constsuccess= Effect.succeed(42)
@see ― fail to create an effect that represents a failure.
@since ― 2.0.0
succeed(
n: number
n+1))
constresult:Effect.Effect<number, never, never>
4
This is very useful when the transformation itself is an effectful computation (i.e. an Effect, that may be async, or fail).
Tip
If you find yourself dealing with an Effect wrapping an Effect (Effect<Effect<A>>), you might have used Effect.map where you should have used Effect.flatMap.
The Effect interface defines a value that describes a workflow or job,
which can succeed or fail.
Details
The Effect interface represents a computation that can model a workflow
involving various types of operations, such as synchronous, asynchronous,
concurrent, and parallel interactions. It operates within a context of type
R, and the result can either be a success with a value of type A or a
failure with an error of type E. The Effect is designed to handle complex
interactions with external resources, offering advanced features such as
fiber-based concurrency, scheduling, interruption handling, and scalability.
This makes it suitable for tasks that require fine-grained control over
concurrency and error management.
To execute an Effect value, you need a Runtime, which provides the
environment necessary to run and manage the computation.
The Effect interface defines a value that describes a workflow or job,
which can succeed or fail.
Details
The Effect interface represents a computation that can model a workflow
involving various types of operations, such as synchronous, asynchronous,
concurrent, and parallel interactions. It operates within a context of type
R, and the result can either be a success with a value of type A or a
failure with an error of type E. The Effect is designed to handle complex
interactions with external resources, offering advanced features such as
fiber-based concurrency, scheduling, interruption handling, and scalability.
This makes it suitable for tasks that require fine-grained control over
concurrency and error management.
To execute an Effect value, you need a Runtime, which provides the
environment necessary to run and manage the computation.
The Effect interface defines a value that describes a workflow or job,
which can succeed or fail.
Details
The Effect interface represents a computation that can model a workflow
involving various types of operations, such as synchronous, asynchronous,
concurrent, and parallel interactions. It operates within a context of type
R, and the result can either be a success with a value of type A or a
failure with an error of type E. The Effect is designed to handle complex
interactions with external resources, offering advanced features such as
fiber-based concurrency, scheduling, interruption handling, and scalability.
This makes it suitable for tasks that require fine-grained control over
concurrency and error management.
To execute an Effect value, you need a Runtime, which provides the
environment necessary to run and manage the computation.
flatMap lets you sequence effects so that the result of one effect can be
used in the next step. It is similar to flatMap used with arrays but works
specifically with Effect instances, allowing you to avoid deeply nested
effect structures.
Since effects are immutable, flatMap always returns a new effect instead of
changing the original one.
When to Use
Use flatMap when you need to chain multiple effects, ensuring that each
step produces a new Effect while flattening any nested effects that may
occur.
Example
import { pipe, Effect } from"effect"
// Function to apply a discount safely to a transaction amount
constapplyDiscount= (
total:number,
discountRate:number
):Effect.Effect<number, Error> =>
discountRate ===0
? Effect.fail(newError("Discount rate cannot be zero"))
flatMap lets you sequence effects so that the result of one effect can be
used in the next step. It is similar to flatMap used with arrays but works
specifically with Effect instances, allowing you to avoid deeply nested
effect structures.
Since effects are immutable, flatMap always returns a new effect instead of
changing the original one.
When to Use
Use flatMap when you need to chain multiple effects, ensuring that each
step produces a new Effect while flattening any nested effects that may
occur.
Example
import { pipe, Effect } from"effect"
// Function to apply a discount safely to a transaction amount
constapplyDiscount= (
total:number,
discountRate:number
):Effect.Effect<number, Error> =>
discountRate ===0
? Effect.fail(newError("Discount rate cannot be zero"))
Effect.flatMap will combine the errors from the original Effect and the Effect used in the callback.
Either<A, B>
Describes a value that may be of two types: A (“right”) and B (“left”). Having these allows representing common scenarios like success/failure or certain unions.
Note
Yes, it si confusing that, looking at the type signature, “right” is the first one, to the left, and left is the second one, to the right. Either<Right, Left>. One way to think about it is: the first argument is the “right” (as in correct) value, whereas the other one is the “not right” value.
You can obtain A with Either.getRight, and B with Either.getLeft. Either.isRight and Either.isLeft are type guards. You guessed it: you can also map with Either.map! There is also Either.mapLeft and Either.mapRight. You will find many more useful methods under the Either namespace.
Tip
Many Effect methods will interpret Either<A, B> as Effect<A, B>, meaning the “left” case is interpreted as a failure. This makes it very easy to work with Either alongside Effect.
Option<T>
Option is defined as Option<A> and allows representing both A and the lack of A. The Option namespace exposes many useful methods to unwrap the value: Option.getOrNull, Option.getOrThrow… You can find out if there is a value or not via Option.isSome or Option.isNone.
Tip
Make sure to import Option from the effect package when working with these methods, since normally Option will already exist as a global in your module, because it’s part of the Web standards.
Tip
Many Effect methods will happily take Option, interpreting the none case as a failure of type NoSuchElementException.