Browse documentation

Fx

Transforming Fx

Turn pushed values into useful domain data without hiding failures, services, or timing.

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.

Fx timelinemap and as emit once for every inputmapas
Operatormap(f) / 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.

Fx timelinefilter keeps admitted values in their input slotsfilter
Operatorfilter(isEven)
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"]
Fx timelinefilterMap omits None and emits each Some in orderfilterMap
OperatorfilterMap(toOption)
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.

Fx timelinemapEffect emits one successful result for each inputmapEffect
OperatormapEffect(loadLabel)
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.

Fx timelinetap observes each value before forwarding ittap
Operatortap(record)
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.