DEV Community

Cover image for Breaking Down Effect TS : Part 2
Modgil
Modgil

Posted on

Breaking Down Effect TS : Part 2

In the previous article, we introduced Effect-TS, a library designed to tackle common challenges in application development, such as handling complex side effects, managing concurrency, and error handling. Effect-TS is ideal for developers who seek robust applications with a focus on consistency, type safety, and advanced concurrency, all within a functional programming paradigm. We briefly discussed the basics of effects, including their type and how to create and execute them.

In this post, we will delve deeper into creating and executing effects, providing an example to build your confidence in using Effect-TS.

Unlike standard TypeScript functions, effects in Effect-TS are neither inherently synchronous nor asynchronous; they simply describe computations that can either succeed or result in errors.

Let's first start with

Effects with Synchronous computations:

Synchronous computations execute sequentially, blocking further execution until they complete, producing immediate and deterministic results.
In Effect-TS, you can represent synchronous computations using Effect.sync.

const generateRandomNumber = Effect.sync(() => Math.random());
Enter fullscreen mode Exit fullscreen mode

Effect.sync always returns a success value. If it throws an error, that indicates a defect rather than a typical effect failure. Therefore, use Effect.sync only when you are certain that your operation will not fail

Executing generateRandomNumber directly will not yield the desired result, as effects are lazy and defer execution until required. To execute the effect and obtain the computation's result, use:

Effect.runSync(generateRandomNumber)
Enter fullscreen mode Exit fullscreen mode

This will execute the sync effect immediately.

When you know that your operation might fail, use Effect.try. This structure allows you to handle exceptions functionally:

const parseJson = (jsonString: string): Effect.Effect<unknown, Error, unknown> =>
  Effect.try({
    try: () => JSON.parse(jsonString),
    catch: (error) => new Error(`Failed to parse JSON: ${(error as Error).message}`),
  });
Enter fullscreen mode Exit fullscreen mode

To execute this effect, run it with Effect.runSync:

const inputString ='{"name": "Alice", "age": 30}';
try {
Effect.runSync(parseJson(inputString)); 
} catch (error) {
  console.error(error);
}
Enter fullscreen mode Exit fullscreen mode

This executes the effect synchronously, catching and logging any parsing errors.

Effects with Asynchronous computations:

Asynchronous computations involve tasks that execute independently of the main program flow, allowing other operations to proceed while waiting for a result.

In Effect-TS, asynchronous computations are managed using the Effect.tryPromise function. This function wraps an asynchronous operation, such as a Promise, into an Effect, allowing you to handle asynchronous processes in a functional, type-safe manner.

const fetchData = (url: string): Effect.Effect<unknown, Error, Response> =>
  Effect.tryPromise({
    try: () => fetch(url), 
    catch: (error) => new Error(`Failed to fetch data: ${(error as Error).message}`),
  });
Enter fullscreen mode Exit fullscreen mode

To execute an asynchronous effect, you use Effect.runPromise, which handles the Promise-based nature of the operation:

const url = "https://api.example.com/data";
Effect.runPromise(fetchData(url))
  .then(data => console.log(data))
  .catch(error => console.error(error));
Enter fullscreen mode Exit fullscreen mode

Avoid Executing Effects Within Other Effects:

Always aim to execute effects at the boundaries of your program, such as in main functions. Running effects inside other effects negates the benefits of using an effect system, as it blurs the clear separation between effect description and execution. By maintaining this separation, you ensure that your program remains modular, predictable, and easier to test and maintain.

Combinators: Combining, Transforming, and Managing Effects

Combinators provide a framework for creating expressive and modular effect descriptions without executing them. By avoiding the nesting of effects and using combinators instead, you leverage the full power of effect systems, aligning with functional programming principles.

Instead of executing effects within other effects:

const initialEffect = Effect.succeed(4);
const transformedEffect = Effect.sync(() => {
  const result = Effect.runSync(initialEffect);
  return result * 2;
});
Enter fullscreen mode Exit fullscreen mode

You can use combinators to maintain separation for e.g.

Effect.map: This takes an effect and lets you change the result it produces using a simple function.

const initialEffect = Effect.succeed(4);
const transformedEffect = Effect.map(initialEffect, x => x * 2);

Enter fullscreen mode Exit fullscreen mode

This transformation does not execute the effect; it only modifies the outcome once the effect is run.

Effect.flatMap: It takes the output of an effect, which produces another effect, and flattens the result. This is commonly used when need to perform sequential computations that result in an effect, allowing the next computation to depend on the value produced by the first effect.

const initialEffect = Effect.succeed(10);
const chainedEffect = Effect.flatMap(initialEffect, x => Effect.succeed(x * 2));
Enter fullscreen mode Exit fullscreen mode

pipe: It allows chaining functions in a left-to-right manner, increasing readability

const pipeline = pipe(
  Effect.succeed(10),
  Effect.map(x => x + 5),
  Effect.map(x => x * 2)
);
Enter fullscreen mode Exit fullscreen mode

and there are many more available to help you handle different scenarios effectively....

Let's build an application called dataTransformer to download data from an S3 bucket, transform it, and then re-upload it back to S3. This example will provide a practical understanding of Effect-TS in action.

First, we create an S3 client and define the functions needed for our tasks:

import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import { Effect, pipe } from 'effect';

const s3Client = 
  new S3Client({
    region: "your-region",
    forcePathStyle: true,
  });

const downloadData = (bucket: string, key: string): Effect.Effect<string, Error> =>
  Effect.tryPromise({
    try: async () => {
      const response = await s3Client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
      return response.Body?.transformToString() ?? "";
    },
    catch: (error) => new Error(`Failed to download data: ${error.message}`),
  });


const transformData = (data: string) =>
  Effect.sync(() => {
    return data.toUpperCase();
  });


const uploadData = (bucket: string, key: string, data: string) =>
  Effect.tryPromise({
    try: () =>
      s3Client.send(
        new PutObjectCommand({
          Bucket: bucket,
          Key: key,
          Body: data,
        })
      ),
    catch: (error) => new Error(`Failed to upload data: ${error.message}`),
  });

Enter fullscreen mode Exit fullscreen mode

Next, we compose the transformer function using Effect-TS combinators:

const dataTransformer = (sourceBucket: string, sourceKey: string, targetBucket: string, targetKey: string) =>
  pipe(
    downloadData(sourceBucket, sourceKey),
    Effect.flatMap(transformData),
    Effect.andThen((transformedData) => uploadData(targetBucket, targetKey, transformedData))
  );
Enter fullscreen mode Exit fullscreen mode

Finally, we define the main function to execute the effectful computation:

Effect.runPromise(dataTransformer("bucket-name", "key", "target-bucket-name", "target-key"))
  .then(() => console.log("Data processed successfully"))
  .catch(console.error);
Enter fullscreen mode Exit fullscreen mode

I hope we are now clear on the creation and execution of effects. Let's continue in the next part, where we'll explore efficient error handling, concurrency management, effect data types, resource management, and much more. Stay tuned!

Top comments (0)