Dual APIs
Most combinators in Effect — Effect.map, Effect.flatMap, Stream.filter,
Stream.map, and many more — are dual: they can be called in two
equivalent ways. You can pass the data first and call the function directly, or
pass the data last and use the function as a step inside pipe. Understanding
both forms lets you read any Effect code fluently and pick the most readable form
for each situation.
import { Effect, pipe } from "effect"
const double = (n: number) => n * 2const effect = Effect.succeed(21)
// data-first: the Effect is the first argument — no pipe needed.const a = Effect.map(effect, double)
// data-last: the Effect is supplied last, by pipe.const b = pipe(effect, Effect.map(double))
// `.pipe` on the Effect itself is the idiomatic form of data-last.const c = effect.pipe(Effect.map(double))All three produce the same Effect<number>. The difference is purely
ergonomic.
How a dual signature is shaped
Section titled “How a dual signature is shaped”The terms “data-first” and “data-last” describe where the self argument (the
data the function operates on) sits. A dual function has both overloads:
import type { Effect } from "effect"
declare const map: { // ┌─── data-last (used by pipe) // ▼ <A, B>(f: (a: A) => B): <E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<B, E, R> // ┌─── data-first (called directly) // ▼ <A, E, R, B>(self: Effect.Effect<A, E, R>, f: (a: A) => B): Effect.Effect<B, E, R>}- The data-last overload takes only the transformation and returns a function
awaiting the data. That returned function is exactly what
pipefeeds the data into. - The data-first overload takes the data and the transformation together and returns the result immediately.
At runtime a single implementation handles both, dispatching on the number of arguments it receives.
Choosing a form
Section titled “Choosing a form”Use the form that reads best for the situation.
When you apply several transformations in sequence, data-last with .pipe keeps
each step on its own line and reads top to bottom:
import { Effect } from "effect"
const program = Effect.succeed(1).pipe( Effect.map((n) => n + 1), Effect.map((n) => n * 10), Effect.tap((n) => Effect.log(`value is ${n}`)))For a single operation, data-first avoids the pipe ceremony:
import { Effect } from "effect"
const program = Effect.map(Effect.succeed(1), (n) => n + 1)Inside Effect.gen
Section titled “Inside Effect.gen”Generators give you a third option that frequently beats both: bind intermediate
results with yield* and write ordinary expressions. There is no pipe and no
nesting.
import { Effect } from "effect"
const program = Effect.gen(function*() { const n = yield* Effect.succeed(1) const doubled = n * 2 yield* Effect.log(`value is ${doubled}`) return doubled})Use yield* for the linear flow; drop into .pipe only when a single value needs
a chain of combinators applied to it.
Writing your own dual functions
Section titled “Writing your own dual functions”When you build a reusable combinator over your own data type, make it dual with
Function.dual so callers get both forms for free. The first argument tells
dual how to recognize a data-first call — either the data-first arity, or a
predicate over the arguments.
import { Function, pipe } from "effect"
// Arity 2 = the data-first overload takes two arguments.const sum = Function.dual< (that: number) => (self: number) => number, // data-last (self: number, that: number) => number // data-first>(2, (self, that) => self + that)
sum(2, 3) // data-first → 5pipe(2, sum(3)) // data-last → 5