Effect.zipLeft

Now that we have seen Effect.zip (39) and Effect.tap (38) , we should be able to understand Effect.zipLeft without much difficulty.

Effect.zipLeft runs both Effects in some arbitrary order (either sequentially or concurrently), just like Effect.zip (39) . Its result type is the result type of the first operation. It discards the result of the second effect, just like Effect.tap (38) .

We can use Effect.zipLeft to run “after-effects”. This might include something like deleting something that is no longer needed, or running some successful completion action. This should not be confused with a resource finalizer, which we will see in future lessons about Scope and resource management.

Here is an example, where we can use Effect.zipLeft after submitting a form. The res object that is put into the cache is the response that came back from the API request, because the return values of the cache and redirect have been discarded.

const addUser = (form: AddUserForm) =>
validateForm(form).pipe(
Effect.flatMap((payload) => Api.post("/v1/users", payload)),
Effect.zipLeft(Cache.invalidate("users:all")),
Effect.zipLeft(Router.redirect("/users")),
Effect.tap((res) => Cache.set(`users:${res.id}`, res))
);

Note that in this case we would not want the invalidation or the redirect to run concurrently. We would not want to redirect the user away from the form if the API request failed. Effect.zipLeft does not run concurrently by default, but it accepts the same options as Effect.zip (39) .

The simplest way to implement Effect.zipLeft is in terms of Effect.zip (39) . Here is a stripped down version.

type ZipLeft = <B>(
that: Effect.Effect<B>
) => <A>(self: Effect.Effect<A>) => Effect.Effect<A>;
const zipLeft: ZipLeft = (that) => (self) =>
self.pipe(
Effect.zip(that),
Effect.map(([a, _b]) => a)
);

Looking at the expression ([a, _b]) => a helps us see why this is called “zip left”. Like most functions in the Effect library, Effect.zipLeft can take self-first, or self-last. Normally in these lessons we show the self-last (pipe-friendly) implementation, but for zipLeft it may be more intuitive to see the self-first version.

type ZipLeftSelfFirst = <A, B>(
self: Effect.Effect<A>,
that: Effect.Effect<B>
) => Effect.Effect<A>;
const zipLeftSelfFirst: ZipLeftSelfFirst = (self, that) =>
self.pipe(zipLeft(that));

Recall that Effect.zip (39) can in turn be implemented using Effect.flatMap (6) or Effect.gen (15) . Here are three more implementations.

const zipLeftFlatMap: ZipLeft = (that) => (self) =>
Effect.flatMap(self, (a) => Effect.flatMap(that, (_b) => Effect.succeed(a)));
const zipLeftYield: ZipLeft = (that) => (self) =>
Effect.gen(function* () {
const a = yield* self;
yield* that;
return a;
});
const zipLeftTap: ZipLeft = (that) => (self) =>
self.pipe(Effect.tap((_b) => that));

All of the above implementations are functionally and theoretically equivalent. They may have different performance characteristics, and some may be more concurrency-friendly than others. You should focus on getting your program to work, and then figure out if it is possible to optimise it. Adding higher concurrency will not necessarily make your application better!

This shows a wider point about Effect: there are lots of equivalent ways of expressing the same functionality. You may find it easier to write an explicit Effect.gen (15) , especially at first. When starting out, you probably won’t use Effect.zipLeft much, because you’ll mostly be thinking in terms of Effect.flatMap (6) or Effect.gen (15) . Over time you may start to see Effect.zipLeft in other people’s code, or begin to notice common places where you could use Effect.zipLeft. This is mostly a matter of stylistic preference.

Let’s recap:

  • Effect.zipLeft combines two effects (sequentially or concurrently)
  • Effect.zipLeft discards the right return value
  • Effect.zipLeft is like a mix of Effect.tap (38) and Effect.zip (39)
  • You don’t need to memorise or use every function in the Effect standard library!