Effect.tap

We have seen a few basic Effect combinators, such as Effect.map (4) and Effect.flatMap (6) . Over the next few lessons we will introduce a few more. These combinators are helpers designed to make your code more readable. You do not need to learn all of these straight away, but they can be very helpful.

Effect.tap allows us to “tap into” the current result mid-way through a pipeline, without being detected. This slightly odd term originally comes from the idea of tapping into a tree to extract sap, but is also similar to the idea of a hacker tapping into a phone line to observe what is happening without being detected.

One of the most common uses is for debugging. If you have a pipeline of Effects, you can tap at any point to observe the current value, and log it out to the console.

import { Context, Effect } from "effect";
const double = (x: number) => x * 2;
const main = Effect.succeed(2).pipe(
Effect.tap((two) => console.log(`initially ${two}`)),
Effect.map(double),
Effect.tap((four) => console.log(`now ${four}`)),
Effect.map(double),
Effect.tap((eight) => console.log(`and now ${eight}`)),
Effect.map(double)
);
Effect.runSync(main);

We can also use this to run side effects. This could include writing to a database, invalidating a cache, or sending a confirmation email.

const placeOrder = (user: User, order: Order) =>
validateOrder(order).pipe(
Effect.map(applyDiscountCode),
Effect.map(calculateShipping(user.shippingAddress)),
Effect.tap(OrderRepository.save),
Effect.tap((order) => sendOrderConfirmation(user.email, order)),
Effect.map((order) => ({
total: order.total,
message:
"Your order has been placed. It will arrive in 3-5 business days.",
}))
);

Note that the signature of Effect.tap is very similar to the signature of Effect.flatMap (6) , only differing in the return type. We can implement Effect.tap in terms of Effect.flatMap (6) .

type Tap = <A, B, E1, R1, E2, R2>(
self: Effect.Effect<A, E2, R2>,
f: (a: A) => Effect.Effect<B, E1, R1>
) => Effect.Effect<A, E1 | E2, R1 | R2>;
const tap: Tap = (self, f) =>
Effect.flatMap(self, (a) => Effect.map(f(a), () => a));

This may be hard to read. Here is the same implementation, written as a generator.

const tapGen: Tap = (self, f) =>
Effect.gen(function* () {
const a = yield* self; // run the first effect
const _b = yield* f(a); // run the second effect
return a; // but return the first result
});

Let’s recap:

  • There are many combinators on top of Effect
  • Most of these can be built in terms of Effect.flatMap (6)
  • Effect.tap lets you run side effects without affecting the next step in the pipe