A catalog feed contains records the page cannot display directly: inactive products, raw cents, and prices that need formatting. The source already decides when records arrive. This lesson turns each record into useful page data without changing who owns the source.
Start with Building Fx. We make decisions from one input alone, then introduce an Effectful callback. Repeated input and clocks have their own time lesson.
Admit a product and build its display value
import { Effect } from "effect";
import { Fx } from "@typed/fx";
interface Product {
readonly id: string;
readonly name: string;
readonly priceInCents: number;
readonly active: boolean;
}
const products = Fx.fromIterable<Product>([
{ id: "desk", name: "Standing desk", priceInCents: 49900, active: true },
{ id: "lamp", name: "Desk lamp", priceInCents: 8900, active: false },
]);
const cards = products.pipe(
Fx.filter((product) => product.active),
Fx.map(({ id, name, priceInCents }) => ({
id,
title: name,
price: `$${(priceInCents / 100).toFixed(2)}`,
})),
);
const result = await Effect.runPromise(Fx.collectAll(cards));
// [{ id: "desk", title: "Standing desk", price: "$499.00" }]
The active desk becomes a card; the inactive lamp produces no output. filter keeps admitted
values unchanged, and map transforms each admitted value. Put admission first so rejected products
do not need formatting.
mapasTick 0. Input: a; Map: f(a); As: value.
Read this diagram
Follow each lane from left to right. Events stacked vertically share a tick; the green cursor marks the current time across every lane.
- A value
The text inside the pill is the emitted value. - Work starts
The raised chevron starts an inner run (^in the source). - The run returns
The vertical bar ends this lane’s run. - A cause is delivered
The exclamation mark belongs to this lane. - Work is interrupted
The cross marks cancellation of this run. - Current time
The line and diamond move together across all lanes. - Happening now
A highlighted event is at the current tick. - Still ahead
Muted, dashed values have not happened yet. - Time continues
The lane’s arrow is not a return marker. An empty stretch can be quiet work that is still running.
Illustrated ticks start at 0. At 1×, one illustrated tick takes one second; captions specify real durations when timing matters. A cause or interruption belongs to its lane, and other work may continue. Scroll horizontally to inspect the rest of a long timeline.
Read vertically: the map output depends on its input; as has the same value in every occupied
slot. Neither removes events.
filterTick 0. Input: 1.
Read this diagram
Follow each lane from left to right. Events stacked vertically share a tick; the green cursor marks the current time across every lane.
- A value
The text inside the pill is the emitted value. - Work starts
The raised chevron starts an inner run (^in the source). - The run returns
The vertical bar ends this lane’s run. - A cause is delivered
The exclamation mark belongs to this lane. - Work is interrupted
The cross marks cancellation of this run. - Current time
The line and diamond move together across all lanes. - Happening now
A highlighted event is at the current tick. - Still ahead
Muted, dashed values have not happened yet. - Time continues
The lane’s arrow is not a return marker. An empty stretch can be quiet work that is still running.
Illustrated ticks start at 0. At 1×, one illustrated tick takes one second; captions specify real durations when timing matters. A cause or interruption belongs to its lane, and other work may continue. Scroll horizontally to inspect the rest of a long timeline.
Transform only when a value is available
Use filterMap when the transformation itself returns an Option. For example, a lookup can produce
a label or omit an unknown ID:
import { Effect, Option } from "effect";
import { Fx } from "@typed/fx";
const names = new Map([["desk", "Standing desk"], ["lamp", "Desk lamp"]]);
const labels = Fx.fromIterable(["desk", "missing", "lamp"]).pipe(
Fx.filterMap((id) => Option.fromNullishOr(names.get(id))),
);
const result = await Effect.runPromise(Fx.collectAll(labels));
// ["Standing desk", "Desk lamp"]
filterMapTick 0. Input: 1.
Read this diagram
Follow each lane from left to right. Events stacked vertically share a tick; the green cursor marks the current time across every lane.
- A value
The text inside the pill is the emitted value. - Work starts
The raised chevron starts an inner run (^in the source). - The run returns
The vertical bar ends this lane’s run. - A cause is delivered
The exclamation mark belongs to this lane. - Work is interrupted
The cross marks cancellation of this run. - Current time
The line and diamond move together across all lanes. - Happening now
A highlighted event is at the current tick. - Still ahead
Muted, dashed values have not happened yet. - Time continues
The lane’s arrow is not a return marker. An empty stretch can be quiet work that is still running.
Illustrated ticks start at 0. At 1×, one illustrated tick takes one second; captions specify real durations when timing matters. A cause or interruption belongs to its lane, and other work may continue. Scroll horizontally to inspect the rest of a long timeline.
The empty slots are omissions, not delayed work. compact handles a source that already emits
Options. See the operator atlas for constant mapping with as and
translating both success and failure with mapBoth.
Make failure explicit with an Effect callback
Pure callbacks should not hide requests or expected parsing errors. mapEffect runs a callback
whose failure and service requirements become part of the resulting Fx:
import { Effect } from "effect";
import { Fx } from "@typed/fx";
const parsePrice = (text: string): Effect.Effect<number, "InvalidPrice"> => {
const price = Number(text);
return text.trim() !== "" && Number.isFinite(price)
? Effect.succeed(price)
: Effect.fail("InvalidPrice" as const);
};
const prices: Fx.Fx<number, "InvalidPrice"> = Fx.fromIterable(["499.00", "89.00"]).pipe(
Fx.mapEffect(parsePrice),
);
const result = await Effect.runPromise(Fx.collectAll(prices));
// [499, 89]
Replacing "89.00" with "unknown" fails collection with InvalidPrice. The earlier emission is
not retracted, but collectAll cannot return a successful array after failure. If the callback
requires a service, that requirement is retained too; services and lifetime
shows how to provide it.
mapEffectTick 0. Input: 1; Output: label-1.
Read this diagram
Follow each lane from left to right. Events stacked vertically share a tick; the green cursor marks the current time across every lane.
- A value
The text inside the pill is the emitted value. - Work starts
The raised chevron starts an inner run (^in the source). - The run returns
The vertical bar ends this lane’s run. - A cause is delivered
The exclamation mark belongs to this lane. - Work is interrupted
The cross marks cancellation of this run. - Current time
The line and diamond move together across all lanes. - Happening now
A highlighted event is at the current tick. - Still ahead
Muted, dashed values have not happened yet. - Time continues
The lane’s arrow is not a return marker. An empty stretch can be quiet work that is still running.
Illustrated ticks start at 0. At 1×, one illustrated tick takes one second; captions specify real durations when timing matters. A cause or interruption belongs to its lane, and other work may continue. Scroll horizontally to inspect the rest of a long timeline.
tapTick 0. Input: 1; Output: 1.
Read this diagram
Follow each lane from left to right. Events stacked vertically share a tick; the green cursor marks the current time across every lane.
- A value
The text inside the pill is the emitted value. - Work starts
The raised chevron starts an inner run (^in the source). - The run returns
The vertical bar ends this lane’s run. - A cause is delivered
The exclamation mark belongs to this lane. - Work is interrupted
The cross marks cancellation of this run. - Current time
The line and diamond move together across all lanes. - Happening now
A highlighted event is at the current tick. - Still ahead
Muted, dashed values have not happened yet. - Time continues
The lane’s arrow is not a return marker. An empty stretch can be quiet work that is still running.
Illustrated ticks start at 0. At 1×, one illustrated tick takes one second; captions specify real durations when timing matters. A cause or interruption belongs to its lane, and other work may continue. Scroll horizontally to inspect the rest of a long timeline.
These rows assume sequential delivery. tap runs an Effect while keeping the original value.
The corresponding admission operators are filterEffect (keep for true) and filterMapEffect
(emit each Some). A failed predicate is not false: it enters the failure
channel. Recovery decides whether that stops the feature.
Effectful transformation inherits producer concurrency. If two callback deliveries overlap, the second lookup may finish first. No queue is added here. Choose an explicit higher-order policy when the requirement is “finish every conversion in order” or “discard obsolete work.”
Operator order is product behavior: normalize before an equality check, and place an observation before or after admission according to what it should count. Continue with stateful transforms for local history, time and rate for debounced search, or Composing Fx to combine independent inputs.