Skip to content

Functional Programming in TypeScript using the fp-ts Library: Exploring Task and TaskEither Operators

Functional Programming in TypeScript using the fp-ts Library: Exploring Task and TaskEither Operators

3 Part Series

Introduction:

Welcome back to our blog series on Functional Programming in TypeScript using the fp-ts library. In the previous three blog posts, we covered essential concepts such as the pipe and flow operators, Option type, and various methods and operators like fold, fromNullable, getOrElse, map, flatten, and chain. In this fourth post, we will delve into the powerful Task and TaskEither operators, understanding their significance, and exploring practical examples to showcase their usefulness.

Understanding Task and TaskEither:

Before we dive into the examples, let's briefly recap what Task and TaskEither are and why they are valuable in functional programming.

Task:

In functional programming, a Task represents an asynchronous computation that may produce a value or an error. It allows us to work with asynchronous operations in a pure and composable manner. Tasks are lazy and only start executing when we explicitly run them. They can be thought of as a functional alternative to Promises.

Now, let's briefly introduce the Either type and its significance in functional programming since this concept, merged with Task gives us the full power of TaskEither.

Either:

Either is a type that represents a value that can be one of two possibilities: a value of type Left or a value of type Right. Conventionally, the Left type represents an error or failure case, while the Right type represents a successful result. Using Either, we can explicitly handle and propagate errors in a functional and composable way.

Example: Handling Division with Either

Suppose we have a function divide that performs a division operation. Instead of throwing an error, we can use Either to handle the potential division by zero scenario. Here's an example:

import { Either, left, right } from 'fp-ts/lib/Either';

const divide: (a: number, b: number) => Either<string, number> = (a, b) => {
  if (b === 0) {
    return left('Error: Division by zero');
  }
  return right(a / b);
};

const result = divide(10, 2);

result.fold(
  (error) => console.log(`Error: ${error}`),
  (value) => console.log(`Result: ${value}`)
);

In this example, the divide function returns an Either type. If the division is successful, it returns a Right value with the result. If the division by zero occurs, it returns a Left value with an error message. We then use the fold function to handle both cases, printing the appropriate message to the console.

TaskEither:

TaskEither combines the benefits of both Task and Either. It represents an asynchronous computation that may produce a value or an error, just like Task, but also allows us to handle potential errors using the Either type. This enables us to handle errors in a more explicit and controlled manner.

Examples:

Let's explore some examples to better understand the practical applications of Task and TaskEither operators.

Example 1: Fetching Data from an API

Suppose we want to fetch data from an API asynchronously. We can use the Task operator to encapsulate the API call and handle the result using the Task's combinators. In the example below, we define a fetchData function that returns a Task representing the API call. We then use the fold function to handle the success and failure cases of the Task. If the Task succeeds, we return a new Task with the fetched data. If it fails, we return a Task with an error message. Finally, we use the getOrElse function to handle the case where the Task returns None.

import { pipe } from 'fp-ts/lib/function';
import { Task } from 'fp-ts/lib/Task';
import { fold } from 'fp-ts/lib/TaskEither';
import { getOrElse } from 'fp-ts/lib/Option';

const fetchData: Task<string> = () => fetch('https://api.example.com/data');

const handleData = pipe(
  fetchData,
  fold(
    () => Task.of('Error: Failed to fetch data'),
    (data) => Task.of(`Fetched data: ${data}`)
  ),
  getOrElse(() => Task.of('Error: Data not found'))
);

handleData().then(console.log);

Example 2: Performing Computation with Error Handling

Let's say we have a function divide that performs a computation and may throw an error. We can use TaskEither to handle the potential error and perform the computation asynchronously. In the example below, we define a divideAsync function that takes two numbers and returns a TaskEither representing the division operation. We use the tryCatch function to catch any potential errors thrown by the divide function. We then use the fold function to handle the success and failure cases of the TaskEither. If the TaskEither succeeds, we return a new TaskEither with the result of the computation. If it fails, we return a TaskEither with an error message. Finally, we use the map function to transform the result of the TaskEither.

import { pipe } from 'fp-ts/lib/function';
import { TaskEither, tryCatch } from 'fp-ts/lib/TaskEither';
import { fold } from 'fp-ts/lib/TaskEither';
import { map } from 'fp-ts/lib/TaskEither';

const divide: (a: number, b: number) => number = (a, b) => {
  if (b === 0) {
    throw new Error('Division by zero');
  }
  return a / b;
};

const divideAsync: (a: number, b: number) => TaskEither<Error, number> = (a, b) =>
  tryCatch(() => divide(a, b), (error) => new Error(String(error)));

const handleComputation = pipe(
  divideAsync(10, 2),
  fold(
    (error) => TaskEither.left(`Error: ${error.message}`),
    (result) => TaskEither.right(`Result: ${result}`)
  ),
  map((result) => `Computation: ${result}`)
);

handleComputation().then(console.log);

In the first example, we saw how to fetch data from an API using Task and handle the success and failure cases using fold and getOrElse functions. This allows us to handle different scenarios, such as successful data retrieval or error handling when the data is not available.

In the second example, we demonstrated how to perform a computation that may throw an error using TaskEither. We used tryCatch to catch potential errors and fold to handle the success and failure cases. This approach provides a more controlled way of handling errors and performing computations asynchronously.

Conclusion:

In this blog post, we explored the Task and TaskEither operators in the fp-ts library. We learned that Task allows us to work with asynchronous computations in a pure and composable manner, while TaskEither combines the benefits of Task and Either, enabling us to handle potential errors explicitly.

By leveraging the concepts we have covered so far, such as pipe, flow, Option, fold, map, flatten, and chain, we can build robust and maintainable functional programs in TypeScript using the fp-ts library. Stay tuned for the next blog post in this series, where we will continue our journey into the world of functional programming.

This Dot Labs is a development consultancy that is trusted by top industry companies, including Stripe, Xero, Wikimedia, Docusign, and Twilio. This Dot takes a hands-on approach by providing tailored development strategies to help you approach your most pressing challenges with clarity and confidence. Whether it's bridging the gap between business and technology or modernizing legacy systems, you’ll find a breadth of experience and knowledge you need. Check out how This Dot Labs can empower your tech journey.

You might also like

Functional Programming in TypeScript Using the fp-ts Library: Deep Dive Into Option's Methods and Other Useful fp-ts Operators cover image

Functional Programming in TypeScript Using the fp-ts Library: Deep Dive Into Option's Methods and Other Useful fp-ts Operators

Welcome back to our blog series on functional programming with fp-ts! In our previous posts, we talked about the building block of fp-ts library: Pipe` and `Flow` operators and we introduced one of the most useful types in the library: `Option` type. Let's start to use our knowledge and combine all the blocks: in this blog post, we'll take a deep dive into fp-ts' `Option` type, and explore its fundamental methods such as `fold`, `fromNullable`, and `getOrElse`. We'll then leverage the `map`, `flatten`, and `chain` operators, combining them with our powerful (and already known) operator to compose expressive and concise code. Understanding Option The Option` type, also known as `Maybe`, represents values that might be absent. It is particularly useful for handling scenarios where a value could be missing, eliminating the need for explicit `null` checks. `fp-ts` equips us with a rich set of methods and operators to work with `Option` efficiently. *fold***: The `fold` method allows us to transform an `Option` value into a different type by providing two functions: one for the `None` case, and another for the `Some` case. The `pipe` operator enhances the readability of the code by enabling a fluent and concise syntax. `ts import { Option, none, some } from 'fp-ts/lib/Option'; import { fold } from 'fp-ts/lib/Option'; import { pipe } from 'fp-ts/lib/pipeable'; const value: Option = some(10); const result = pipe( value, fold( () => 'No value', (x: number) => Value is ${x}` ) ); // result: "Value is 10" ` In this example, we have an Option` value `some(10)`, representing the presence of the number 10. We use the `pipe` operator from `fp-ts` to chain the value through the `fold` function, passing in two functions. The first function, `() => 'No value'`, handles the `None` case when the Option is empty. The second function, `(x: number) => Value is ${x}`, handles the `Some` case and receives the value inside the `Option` (in this case, 10). The resulting value is "Value is 10". *fromNullable***: The `fromNullable` function converts nullable values (e.g., `null` or `undefined`) into an `Option`. We can leverage `pipe` to make the code more readable and maintainable. `ts import { Option, fromNullable } from 'fp-ts/lib/Option'; import { pipe } from 'fp-ts/lib/pipeable'; const value: string | null = 'Hello, world!'; const optionValue: Option = pipe(value, fromNullable); ` In the example, we have a string value 'Hello, world!'`, which is not nullable. However, by using the `pipe` operator and passing the value through `fromNullable`, fp-ts internally checks if the value is null or undefined. If it is, it produces a `None` value, indicating the absence of a value. Otherwise, it wraps the value inside `Some`. So, in this case, the resulting `optionValue` is `Some("Hello, world!")`. *getOrElse***: The `getOrElse` method allows us to extract the value from an `Option` or provide a default value if the `Option` is `None`. `Pipe` operator aids in composing the `getOrElse` function with other operations seamlessly. `ts import { Option, some, none } from 'fp-ts/lib/Option'; import { getOrElse } from 'fp-ts/lib/Option'; import { pipe } from 'fp-ts/lib/pipeable'; const optionValue: Option = some(10); const value = pipe(optionValue, getOrElse(() => 0)); // value: 10 const noneValue: Option = none; const defaultValue = pipe(noneValue, getOrElse(() => 0)); // defaultValue: 0 ` In the first example, we have an Option value some(10)`. Using the `pipe` operator, and passing the Option through `getOrElse`, we provide a function `() => 0` as a default value. Since the Option is `Some(10)`, the function is not executed, and the resulting value is `10`. In the second example, we have an Option value `none`, representing the absence of a value. Again, using the `pipe` operator and `getOrElse`, we provide a default value of `0`. Since the Option is `None`, the function `() => 0` is executed, resulting in the default value of `0`. Map, Flatten, and Chain Operators Building upon the foundational methods of Option`, `fp-ts` provides powerful operators like `map`, `flatten`, and `chain`, which enable developers to compose complex operations in a functional and expressive manner. *map***: The map operator allows us to transform the value inside an `Option` using a provided function. It applies the function only if the `Option` is `Some`. `ts import { Option, some } from 'fp-ts/lib/Option'; import { map } from 'fp-ts/lib/Option'; import { pipe } from 'fp-ts/lib/pipeable'; const optionValue: Option = some(10); const mappedValue: Option = pipe(optionValue, map((x: number) => Value is ${x}`)); // mappedValue: Some("Value is 10") ` In this example, we have an Option` value `some(10)`. Using the `pipe` operator and passing the `Option` through `map`, we provide a function `(x: number) => Value is ${x}`. Since the `Option` is `Some(10)`, the function is applied to the value inside the `Option`, resulting in a new `Option` `Some("Value is 10")`. *flatten***: The `flatten` operator allows us to `flatten` nested `Options` into a single `Option`. It simplifies the resulting structure when we have computations that may produce an `Option` inside another `Option`. The `pipe` operator assists in composing `flatten` operations seamlessly. `ts import { Option, some, none } from 'fp-ts/lib/Option'; import { flatten } from 'fp-ts/lib/Option'; import { pipe } from 'fp-ts/lib/pipeable'; const nestedOption: Option> = some(some(10)); const flattenedOption: Option = pipe(nestedOption, flatten); // flattenedOption: Some(10) ` In the example, we have a nested Option` `some(some(10))`. Using the `pipe` operator and passing the nested `Option` through `flatten`, `fp-ts` flattens the structure, resulting in a single Option `Some(10)`. *chain***: The `chain` operator, also known as `flatMap` or `>>=`, combines the functionalities of `map` and `flatten`. It allows us to apply a function that produces an `Option` to the value inside an `Option`, resulting in a flattened `Option`. `ts import { Option, some, none } from 'fp-ts/lib/Option'; import { chain } from 'fp-ts/lib/Option'; import { pipe } from 'fp-ts/lib/pipeable'; const optionValue: Option = some(42); const chainedValue: Option = pipe( optionValue, chain((x: number) => (x > 10 ? some(Value is ${x}`) : none)) ); // chainedValue: Some("Value is 42") const noneValue: Option = none; const noneChainedValue: Option = pipe( noneValue, chain((x: number) => (x > 10 ? some(Value is ${x}`) : none)) ); // noneChainedValue: None ` In the first example, we have an Option value some(42)`. Using the `pipe` operator and passing the `Option` through `chain`, we provide a function that checks if the value is greater than 10. If it is, it returns `Some(Value is ${x})`, where `x` is the value inside the `Option`. Since the value is 42, which is greater than 10, the resulting Option is `Some("Value is 42")`. In the second example, we have an `Option` value `none`, representing the absence of a value. When passing it through `chain` with the same function as before, the function is not executed because the `Option` is `None`, resulting in `None`. Conclusion fp-ts` provides powerful methods and operators for working with the `Option` type, allowing developers to embrace functional programming principles effectively. By understanding the `fold`, `fromNullable`, and `getOrElse` methods, as well as the `map`, `flatten`, and `chain` operators, and combining them with the `pipe` operator, developers can write expressive, maintainable, and resilient code. Explore these tools, unlock their potential, and take your functional programming skills to the next level!...

Drizzle ORM: A performant and type-safe alternative to Prisma cover image

Drizzle ORM: A performant and type-safe alternative to Prisma

Introduction I’ve written an article about a similar, more well-known TypeScript ORM named Prisma in the past. While it is a fantastic library that I’ve used and have had success with personally, I noted a couple things in particular that I didn’t love about it. Specifically, how it handles relations with add-on queries and also its bulk that can slow down requests in Lambda and other similar serverless environments. Because of these reasons, I took notice of a newer player in the TypeScript ORM space named Drizzle pretty quickly. The first thing that I noticed about Drizzle and really liked is that even though they call it an ‘ORM’ it’s more of a type-safe query builder. It reminds me of a JS query builder library called ‘Knex’ that I used to use years ago. It also feels like the non-futuristic version of EdgeDB which is another technology that I’m pretty excited about, but committing to it still feels like a gamble at this stage in its development. In contrast to Prisma, Drizzle is a ‘thin TypeScript layer on top of SQL’. This by default should make it a better candidate for Lambda’s and other Serverless environments. It could also be a hard sell to Prisma regulars that are living their best life using the incredibly developer-friendly TypeScript API’s that it generates from their schema.prisma files. Fret not, despite its query-builder roots, Drizzle has some tricks up its sleeve. Let’s compare a common query example where we fetch a list of posts and all of it’s comments from the Drizzle docs: ` // Drizzle query const posts = await db.query.posts.findMany({ with: { comments: true, }, }); // Prisma query const posts = await prisma.post.findMany({ include: { comments: true, }, }); ` Sweet, it’s literally the same thing. Maybe not that hard of a sale after all. You will certainly find some differences in their APIs, but they are both well-designed and developer friendly in my opinion. The schema Similar to Prisma, you define a schema for your database in Drizzle. That’s pretty much where the similarities end. In Drizzle, you define your schema in TypeScript files. Instead of generating an API based off of this schema, Drizzle just infers the types for you, and uses them with their TypeScript API to give you all of the nice type completions and things we’re used to in TypeScript land. Here’s an example from the docs: ` import { integer, pgEnum, pgTable, serial, uniqueIndex, varchar } from 'drizzle-orm/pg-core'; // declaring enum in database export const popularityEnum = pgEnum('popularity', ['unknown', 'known', 'popular']); export const countries = pgTable('countries', { id: serial('id').primaryKey(), name: varchar('name', { length: 256 }), }, (countries) => { return { nameIndex: uniqueIndex('nameidx').on(countries.name), } }); export const cities = pgTable('cities', { id: serial('id').primaryKey(), name: varchar('name', { length: 256 }), countryId: integer('countryid').references(() => countries.id), popularity: popularityEnum('popularity'), }); ` I’ll admit, this feels a bit clunky compared to a Prisma schema definition. The trade-off for a lightweight TypeScript API to work with your database can be worth the up-front investment though. Migrations Migrations are an important piece of the puzzle when it comes to managing our applications databases. Database schemas change throughout the lifetime of an application, and the steps to accomplish these changes is a non-trivial problem. Prisma and other popular ORMs offer a CLI tool to manage and automate your migrations, and Drizzle is no different. After creating new migrations, all that is left to do is run them. Drizzle gives you the flexibility to run your migrations in any way you choose. The simplest of the bunch and the one that is recommended for development and prototyping is the drizzle-kit push command that is similar to the prisma db push command if you are familiar with it. You also have the option of running the .sql files directly or using the Drizzle API's migrate function to run them in your application code. Drizzle Kit is a companion CLI tool for managing migrations. Creating your migrations with drizzle-kit is as simple as updating your Drizzle schema. After making some changes to your schema, you run the drizzle-kit generate command and it will generate a migration in the form of a .sql file filled with the needed SQL commands to migrate your database from point a → point b. Performance When it comes to your database, performance is always an extremely important consideration. In my opinion this is the category that really sets Drizzle apart from similar competitors. SQL Focused Tools like Prisma have made sacrifices and trade-offs in their APIs in an attempt to be as database agnostic as possible. Drizzle gives itself an advantage by staying focused on similar SQL dialects. Serverless Environments Serverless environments are where you can expect the most impactful performance gains using Drizzle compared to Prisma. Prisma happens to have a lot of content that you can find on this topic specifically, but the problem stems from cold starts in certain serverless environments like AWS Lambda. With Drizzle being such a lightweight solution, the time required to load and execute a serverless function or Lambda will be much quicker than Prisma. Benchmarks You can find quite a few different open-sourced benchmarks of common database drivers and ORMs in JavaScript land. Drizzle maintains their own benchmarks on GitHub. You should always do your own due diligence when it comes to benchmarks and also consider the inputs and context. In Drizzle's own benchmarks, it’s orders of magnitudes faster when compared to Prisma or TypeORM, and it’s not far off from the performance you would achieve using the database drivers directly. This would make sense considering the API adds almost no overhead, and if you really want to achieve driver level performance, you can utilize the prepared statements API. Prepared Statements The prepared statements API in Drizzle allows you to pre-generate raw queries that get sent directly to the underlying database driver. This can have a very significant impact on performance, especially when it comes to larger, more complex queries. Prepared statements can also provide huge performance gains when used in serverless environments because they can be cached and reused. JOINs I mentioned at the beginning of this article that one of the things that bothered me about Prisma is the fact that fetching relations on queries generates additional sub queries instead of utilizing JOINs. SQL databases are relational, so using JOINs to include data from another table in your query is a core and fundamental part of how the technology is supposed to work. The Drizzle API has methods for every type of JOIN statement. Properly using JOINs instead of running a bunch of additional queries is an important way to get better performance out of your queries. This is a huge selling point of Drizzle for me personally. Other bells and whistles Drizzle Studio UIs for managing the contents of your database are all the rage these days. You’ve got Prisma Studio and EdgeDB UI to name a couple. It's no surprise that these are so popular. They provide a lot of value by letting you work with your database visually. Drizzle also offers Drizzle Studio and it’s pretty similar to Prisma Studio. Other notable features - Raw Queries - The ‘magic’ sql operator is available to write raw queries using template strings. - Transactions - Transactions are a very common and important feature in just about any database tools. It’s commonly used for seeding or if you need to write some other sort of manual migration script. - Schemas - Schemas are a feature specifically for Postgres and MySQL database dialects - Views -Views allow you to encapsulate the details of the structure of your tables, which might change as your application evolves, behind consistent interfaces. - Logging - There are some logging utilities included useful for debugging, benchmarking, and viewing generated queries. - Introspection - There are APIs for introspecting your database and tables - Zod schema generation - This feature is available in a companion package called drizzle-zod that will generate Zod schema’s based on your Drizzle tables Seeding At the time of this writing, I’m not aware of Drizzle offering any tools or specific advice on seeding your database. I assume this is because of how straightforward it is to handle this on your own. If I was building a new application I would probably provide a simple seed script in JS or TS and use a runtime like node to execute it. After that, you can easily add a command to your package.json and work it into your CI/CD setup or anything else. Conclusion Drizzle ORM is a performant and type-safe alternative to Prisma. While Prisma is a fantastic library, Drizzle offers some advantages such as a lightweight TypeScript API, a focus on SQL dialects, and the ability to use JOINs instead of generating additional sub queries. Drizzle also offers Drizzle Studio for managing the contents of your database visually, as well as other notable features such as raw queries, transactions, schemas, views, logging, introspection, and Zod schema generation. While Drizzle may require a bit more up-front investment in defining your schema, it can be worth it for the performance gains, especially in serverless environments....

Functional Programming in TypeScript using the fp-ts library: Pipe and Flow Operator cover image

Functional Programming in TypeScript using the fp-ts library: Pipe and Flow Operator

Functional programming (FP) is an approach to software development that uses pure functions to help developers in trying to create maintainable and easy-to-test software. FP is notable for its ability to efficiently parallelize pure functions. This approach should lead to code that is easier to reason about and test, and can often result in more concise and maintainable code. ## What you will find in this post Reading about “pure functions” or “parallelizing” things could be scary. Everything seems very “mathematical” and “theoretical”, but don’t be afraid: the goal of this post is to try to explain in a really practical way (theory and math will pop out only when really necessary) how to bring these core concepts and approach of FP in your TS project using Giulio Canti’s library fp-ts**. Pure and Impure functions Pure functions always produce the same output for the same input, regardless of the context in which it is called. Calling this type of function also has no side effects, meaning it does not modify any external state or interact with the outside world in any way. Here is an example of such pure function: `ts function doubleNumbers(numbers: number[]): number[] { return numbers.map((n) => n 2); } ` This function takes an array of numbers, doubles each number in the array, and returns a new array with the doubled numbers. Here are some characteristics of this function that make it pure: 1. No side effects**: This function doesn't modify any external state outside of its own scope. It only operates on the input array, and returns a new array with the transformed values. 2. Deterministic**: Given the same input, this function will always return the same output. There are no random or unpredictable behaviors. 3. No dependencies**: This function doesn't rely on any external state or dependencies. It only depends on the input that's passed to it. Because of these characteristics, this function is considered pure. It's predictable, testable, and doesn't introduce any unexpected behavior. A side effect occurs in a program when you insert external code into your function. This prevents the function from “purely” performing its task. An impure function in TypeScript is a function that, when called with the same arguments, may produce different results each time it is called. This is because an impure function can depend on mutable state or external factors, such as the current time, user input, or global variables, which can change between calls. Here's an example of an impure function in TypeScript: `ts let counter = 0; function incrementCounter(value: number): number { counter += value; return counter; } ` In this example, the incrementCounter` function modifies the global `counter` variable every time it is called. Since the function's return value depends on the current value of `counter`, calling the function with the same argument multiple times may produce different results. For example, if we call the function like this: `ts incrementCounter(1); // returns 1 incrementCounter(1); // returns 2 incrementCounter(1); // returns 3 ` The function's return value changes each time we call it because the value of counter` is being modified by each call. This makes incrementCounter an impure function. Note that impure functions can make it harder to reason about code since their behavior can be unpredictable. In general, it's best to minimize the use of impure functions and rely on pure functions, which have no side effects and always produce the same output for a given input. ## Actions, Calculations, and Data - Thinking like a functional programmer Let’s try to focus one moment on side effects. The definition says FP avoids side effects, but side effects are the very reason we run our software. The definition seems to imply that we must try to completely avoid them when in reality, we use side effects when we have to. Functional programmers know side effects are necessary yet problematic, so they have a lot of tools for working with them. A functional programmer first tries to classify the code into 3 general categories: - Actions - Calculations - Data Actions** depends on when they are called or how many times they are called. Calculations** and **Data** instead doesn’t depend on when or how many times they are called, the difference is only that calculations can be executed, data cannot; you don’t really know what calculation will do until you run it. Functional programmers prefer data to calculations and calculations to actions and they try to build code by applying and composing** as many “calculations” as possible in order to work with "solid" data (but also trying to “control” actions too). ## Fp-Ts There are many famous functional programming languages such as Haskell, Scala, Elm, Erlang, Elixir etc. Fp-Ts provides developers with popular patterns and reliable abstractions from typed functional languages in TypeScript. In this post, we will focus on the first two building blocks of fp-ts**: `Pipe` and `Flow` operators. ## Pipe Operator The pipe operator is a built-in function in fp-ts that allows you to compose functions from left to right. It takes a value as its first argument and any number of functions as subsequent arguments (that accept only one argument of the same type of the returning type of the preceding function). The output of each function becomes the input of the next function in the pipeline, and the final result is returned. `ts import { pipe } from 'fp-ts/function'; const result = pipe( 1, increment, double, decrement ); function increment(n: number): number { return n + 1; } function double(n: number): number { return n 2; } function decrement(n: number): number { return n - 1; } ` In this example, we start with the number. Then increment it, double it, and finally decrement it. The pipe operator allows us to chain these functions together in a readable and concise way. Why use the pipe operator? The pipe operator is useful for a few reasons: 1. It makes code more readable. By chaining functions together with the pipe operator, it is easy to see the flow of data through the functions. 2. It makes code more concise. Without the pipe operator, we would have to nest function calls, which can be hard to read and understand. 3. It makes code more modular. By breaking down a problem into smaller, composable functions, we can write more reusable code. ## Flow Operator We could say that the pipe` operator is our basic brick to build our ecosystem, `flow` is really similar but it allows you to define a sequence of functions separately and then apply them to a value later. The flow operator is also a built-in function in fp-ts, it takes any number of functions as arguments and returns a new function that applies each function in order. `ts import { flow } from 'fp-ts/function'; const addOne = (x: number) => x + 1; const double = (x: number) => x 2; const addOneAndDouble = flow(addOne, double); console.log(addOneAndDouble(2)); // Output: 6 ` In the example above, we create two simple functions addOne` and `double` that takes a number and return a number. We then use the `flow` function from fp-ts to compose the two functions. The `flow` function takes any number of functions as arguments and returns a new function that applies each function in sequence from left to right. By using the Flow` operator, we don't need to explicitly define the type of the `addOne` and `double` functions' parameters. TypeScript can **infer** their types based on how they are used in the function **composition**. This makes the code shorter and more readable while still ensuring type safety. Pros** of using the `Flow` operator in fp-ts: 1. Type inference: The Flow operator allows TypeScript to infer the types of function parameters, making the code shorter and more readable while still ensuring type safety. 2. Composability: The Flow function provided by fp-ts makes it easy to compose multiple functions into a single function. Cons** of using the `Flow` operator in fp-ts: 1. Learning curve: If you are new to functional programming, the Flow operator can be challenging to understand and use correctly. 2. Debugging: Since the Flow operator relies on type inference, it can be challenging to debug issues related to type mismatches or incorrect function composition. ## Differences between pipe and flow The key difference between pipe and flow is in how they handle type inference. Pipe` is better at inferring types, as it can use the output type of one function as the input type of the next function in the pipeline. `Flow`, on the other hand, requires explicit type annotations to be added to the composed functions. ## When to use Pipe or Flow Both pipe` and `flow` are useful tools for composing functions and creating pipelines in a functional and declarative way. However, there are some situations where one might be more appropriate than the other. Pipe` is a good choice when you need to perform a **linear sequence of transformations** on a value in left-to-right order. Flow` is a good choice when you need to create a **reusable function that encapsulates a series of transformations**. Conclusion Fp-ts is a powerful library that provides many tools and abstractions for functional programming in TypeScript. The pipe` and `flow` operators are just two of the many features that it offers. In this post we started to explore the building blocks of this library, learning their powers and their main differences. If you're interested in learning more about functional programming, explore some of these other concepts and check out the fp-ts documentation for more information, or wait for the next blog post here on **This Dot Blog**!...

Software Team Leadership: Risk Taking & Decision Making with David Cramer, Co-Founder & CTO at Sentry cover image

Software Team Leadership: Risk Taking & Decision Making with David Cramer, Co-Founder & CTO at Sentry

In this episode of the engineering leadership series, Rob Ocel interviews David Cramer, co-founder and CTO of Sentry, delving into the importance of decision-making, risk-taking, and the challenges faced in the software engineering industry. David emphasizes the significance of having conviction and being willing to make decisions, even if they turn out to be wrong. He shares his experience of attending a CEO event, where he discovered that decision-making and conflict resolution are struggles even for successful individuals. David highlights the importance of making decisions quickly and accepting the associated risks, rather than attempting to pursue multiple options simultaneously. He believes that being decisive is crucial in the fast-paced software engineering industry. This approach allows for faster progress and adaptation, even if it means occasionally making mistakes along the way. The success of Sentry is attributed to a combination of factors, including market opportunity and the team's principles and conviction. David acknowledges that bold ideas often carry a higher risk of failure, but if they do succeed, the outcome can be incredibly significant. This mindset has contributed to Sentry’s achievements in the industry. The interview also touches on the challenges of developing and defending opinions in the software engineering field. David acknowledges that it can be difficult to navigate differing viewpoints and conflicting ideas. However, he emphasizes the importance of standing by one's convictions and being open to constructive criticism and feedback. Throughout the conversation, David emphasizes the need for engineering leaders to be decisive and take calculated risks. He encourages leaders to trust their instincts and make decisions promptly, even if they are uncertain about the outcome. This approach fosters a culture of innovation and progress within engineering teams. The episode provides valuable insights into the decision-making process and the challenges faced by engineering leaders. It highlights the importance of conviction, risk-taking, and the ability to make decisions quickly in the software engineering industry. David's experiences and perspectives offer valuable lessons for aspiring engineering leaders looking to navigate the complexities of the field....