lloydrichards.dev

EFFECT

FUNCTIONAL-PROGRAMMING

FP-TS

February 07, 2025

Effect: The Basics (Part 1)

Exploring Effect as an evolution of my use of fp-ts.

Very early in my development career I was introduced to functional programming through Professor Fisby's Mostly Adequate Guide to Functional Programming and then the adoptions of fp-ts and io-ts in my day-to-day work. I've been using fp-ts for a little over five years now and has been a fundamental part of my development process.

I even have a whole series of lab posts from two years ago diving deeper into the subject and improving my skills.

So last year I was a little sad / apprehensive to see the Giulio Canti announcement (cabd677) that fp-ts future development would be in the Effect ecosystem, and that the fp-ts library would be only maintained going forward. I've put off learning Effect mostly because of the resistance I already get for using fp-ts in my day-to-day work. But I've decided to take the plunge and start learning Effect and see how it compares to fp-ts.

What is Effect?

TypeScript/JavaScript, the most popular programming language, is still missing a standard library. Effect is filling this gap by providing a solid foundation of data structures, utilities, and abstractions to make building applications easier.

Once you get past all the tag-lines and fluff, Effect is essentially a pattern of writing code that has strong focusing on addressing the complexity of writing applications in TypeScript/JavaScript. It's a library that provides a set of data structures, utilities, and abstractions to make building applications easier to write, read, test, observe, and (ultimately) maintain.

This means its kind of hard to compare when looking at "simple" examples, cause the "simple" way of doing things is already easy. But in reality, most programs are not made complex by the accumulation of simple things, but by continuous creep and ambiguity of the development process and maintenance. Effect offers a kind of common hand-shake between developers (past me and future me) to help make the logic of the program more explicit, easier to understand, and (hopefully) more maintainable.

Overlap with fp-ts

Its easy to see the overlap when you start looking at the examples with all the pipe functions, Either types, and .match function. But there are also clear differences with how Effect approaches problem solving such as the more implicit Effect.gen that use of JS generators to allow for a more "conventional" look.

While I wait patiently for the pipe operator to be added to JavaScript (TC39 spec), the pipe function is still my favorite way to write code in fp-ts and now in Effect.

import { pipe, Array } from "effect";
 
const double = (n: number): number => n * 2;
const square = (n: number): number => n * n;
const inc = (n: number): number => n + 1;
 
const program = pipe(
  [1, 2, 3, 4, 5],
  Array.map(double),
  Array.map(inc),
  Array.map(square),
  Array.map(double),
  Array.map(inc),
);
 
console.log(program); // <- [ 19, 51, 99, 163, 243 ]

This, in my mind, looks so much cleaner then:

const programGen = () => {
  const values = [1, 2, 3, 4, 5];
  const doubled = values.map(double);
  const incremented = doubled.map(inc);
  const squared = incremented.map(square);
  const doubled2 = squared.map(double);
  const incremented2 = doubled2.map(inc);
  return incremented2;
};
 
console.log(program); // <- [ 19, 51, 99, 163, 243 ]

Primitives

Since I have a bunch of examples in fp-ts: The Basics (Part 1), this seems like a good way to make the comparison between fp-ts and Effect. Not all the examples will apply, but I'll jot down my thoughts for the ones that do.

Option

Effect has a drop in replacement for Option that is very similar to fp-ts. With the pipe function there is almost no change apart from the import and name which is really nice for these little functions.

import { Option, pipe } from "effect";
 
const double = (x: number): number => x * 2;
const square = (x: number): number => x ** 2;
 
const result = pipe(
  Option.some(3),
  Option.map(double),
  Option.map(square),
  Option.map(double),
);
 
console.log(result); // Output: Some(72)

Either

Very similar to Option, Effect has a near drop in replacement for Either. The major caveat though is that the type signature is a little backwards. In fp-ts the Either type is Either<Left, Right> where Left is typically an error type and Right is the success type. But in Effect this is backwards to match Effect<Value, Error, Context> pattern. This is a little confusing at first, but once you get used to it, it's not that bad.

There are also few methods, such as the .fromPredicate() that need be replaces with the .liftPredicate() function. But the pipe function is still the same.

import { Either, flow, pipe } from "effect";
 
const isPositive = (x: number): boolean => x > 0;
const isNotZero = (x: number): boolean => x !== 0;
 
const divide = (x: number, y: number): Either.Either<number, string> =>
  pipe(
    y,
    Either.liftPredicate(isNotZero, () => "Division by zero"),
    Either.map((y) => x / y),
  );
 
const squareRoot = flow(
  Either.liftPredicate<number, string>(
    isPositive,
    () => "Cannot calculate square root of a negative number",
  ),
  Either.map((x) => Math.sqrt(x)),
);
 
const calculate = flow(
  divide,
  Either.andThen((value) => squareRoot(value)),
);
 
console.log(calculate(10, 2)); // Output: Right(2.5)
console.log(calculate(10, 0)); // Output: Left(Division by zero)
console.log(calculate(-10, 2)); // Output: Left("Cannot calculate square root of a negative number")

Task

This was where I thought things were going to start to get a little different. In fp-ts the Task type is used to represent an asynchronous computation that can fail. But in Effect, its more about how the Effect type is run using Effect.runPromise(). I actually find this much easier to understand, as figuring out when to run the Task in fp-ts was always a little tricky.

Another nice DX improvement is the object structure of the Effect.tryPromise() which makes it implicit what is happening. There was also some nice validation checks like the parse function below not using .tryPromise() since the JSON.parse function is synchronous. When tried to use .tryPromise() it throw an error.

import { Effect, pipe } from "effect";
 
const fetchData = (url: string) =>
  Effect.tryPromise({
    try: () => fetch(url).then((res) => res.text()),
    catch: (e) => `Error fetching data, ${String(e)}`,
  });
 
const parse = (str: string) =>
  Effect.try({
    try: () => JSON.parse(str),
    catch: (e) => `Error parsing data, ${String(e)}`,
  });
 
const stringify = (obj: unknown) =>
  Effect.tryPromise({
    try: async () => JSON.stringify(obj),
    catch: (e) => `Error stringify-ing data, ${String(e)}`,
  });
 
const getJson = () =>
  pipe(
    fetchData("/api/healthcheck"),
    Effect.andThen(parse),
    Effect.andThen(stringify),
    Effect.runPromise,
  );
 
getJson().then(console.log).catch(console.error); // Output: Right("{"status":"ok","timestamp":"2025-02-07T10:01:07.568Z"}")

Composition

For the most part composition has already been covered with pipe and flow working in the same way. But there are some nice additions to the Effect library that make it easier to compose functions together. As well as the option to create generators with Effect.gen.

matching

Pattern matching was one place I noticed a different where the Option.match() accepts an object with onNone and onSome keys. This is actually a nice improvement and makes the code a little easier to read. The same can be said for other functions like Effect.catchTags() which can map out the Error keys to a more readable format.

import { flow, Option } from "effect";
 
const double = (x: number): number => x * 2;
const square = (x: number): number => x ** 2;
 
const result = flow(
  Option.fromNullable<number>,
  Option.map(double),
  Option.map(square),
  Option.match({
    onNone: () => "No value",
    onSome: double,
  }),
);
 
console.log(result(3)); // Output: 72
console.log(result(undefined)); // Output: No value

Conclusion

Its still early days in the adoption of Effect, but so far I'm enjoying learning it and seeing how it compares to fp-ts. I'm not sure if I'll be able to use it in my day-to-day work, but hopefully I can sprinkle it in here and there to see how it goes.

There are still a bunch of things I want to explore like validation (Schema), dependency injection (Effect.Context), and observability (Effect.withSpan()) so I'm sure there will be more parts to come.