What is the best approach for managing interactions across various impure and asynchronous services using a functional programming methodology?

Trying to learn functional programming on my own has been a bit challenging. Most of the resources available are either too basic (“This is an Option!” “this is an Either!”) or so complex that I can't tell if they're serious or just joking.

Let's consider a practical hypothetical scenario:

The goal of the function is to create an endpoint that takes a file name and a string, then writes it to a static store. The process involves:

  1. Checking if the user already has more than 10 files stored, and if so, returning false
  2. Recording the file name in the database
  3. Writing the file to the file system
  4. Logging the transaction
  5. Returning true

There are three external systems involved: the database, the file system, and the logger. An initial simplistic implementation could look like this:

const USER_FILE_MAX = 10;
const saveFile = async (user: User, filename: string, contents: string): boolean => {
  if (await db.fetchFileCount(user) >= USER_FILE_MAX) {
    return false;
  }
  const success = await db.transaction(async () => {
    await db.writeFilename(user, filename);
    await fs.writeFile(filename, contents);
   });
   logger.log(`${user}.name wrote ${filename} ${success? "un" : ""}successfully`);
   return success;
};

However, from a functional programming perspective, this approach is not ideal. I'm struggling with how to refactor the function and services to achieve a more functional, readable, and testable solution.

I've come across suggestions to use IO monads, Effects monads, or Tagless-final monads, but I'm having difficulty implementing any of these options effectively.

If you have any suggestions, please feel free to share them.

Answer №1

For handling network calls, I recommend using TaskEither as it acts like a lazily-evaluated promise that returns an Either (allowing for error handling and always resolving). For file IO operations, IOEither would be the preferred choice. According to fp-ts, Task and IO are suggested as monads for asynchronous and synchronous tasks.

import * as TE from 'fp-ts/TaskEither'
declare const dbWriteFilename: (user: User, filename: string, t?: Transaction) => TaskEither<SomeDbError, void>
declare const dbFetchFileCount: (user: User, t?: Transaction) => TaskEither<SomeDbError, number>
declare const runInTransaction: <A>(thunk: (t: Transaction) => TaskEither<SomeDbError, A>) => TaskEither<SomeDbError, A>
declare const writeFile: (filename: string, contents: string) => IOEither<SomeFileError, void>

const USER_FILE_MAX = 10;

const saveFile = (user: User, filename: string) => runInTransaction(t => pipe(
  dbFetchFileCount(user, t),
  TE.chain(TE.fromPredicate(count => count < USER_FILE_MAX, () => 'too many files')),
  TE.chain(() => dbWriteFilename(user, filename, t)),
  TE.chainIOEither(() => writeFile(filename, contents))
))()

If at any point within this process a TaskEither fails, it will skip subsequent chain functions and only execute actions in getOrElse or mapLeft functions that affect the Left type of TaskEither.

In TaskEither conventions, the Left type signifies errors while the Right type denotes successful outcomes.

This setup allows you to eliminate the need for returning true/false and opt for something like:

if(Either.isRight(await safeFile(...)()) {
  // success!
}

Here is a complete example:

pipe(
  saveFile(...),
  TE.fold(
    e => console.error('Whoops!', e),
    () => console.log('Great success!')
  )
)()

The fold function takes a TaskEither and returns a Task (in this scenario, it would take a TaskEither<SomeError, void> and return a Task since both functions within fold return void).

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What data structure is used to store HTML elements in TypeScript?

Currently, I am dealing with a typescript variable that holds the outcome of a query on the DOM: let games = document.getElementsByTagname("game"); My uncertainty lies in identifying the appropriate type for the resulting array. Should I expect an array ...

Are 'const' and 'let' interchangeable in Typescript?

Exploring AngularJS 2 and Typescript led me to create something using these technologies as a way to grasp the basics of Typescript. Through various sources, I delved into modules, Typescript concepts, with one particularly interesting topic discussing the ...

Using an external npm module in TypeScript can result in the tsc output directory being modified

In my TypeScript project, I have set up the build process to generate JavaScript files in the ./src/ directory. Everything works smoothly when building against existing npm modules, such as Angular 2 imports. However, I encountered a strange issue when I ...

Variations in key options based on specific situations

Is it possible to make certain keys required in Typescript depending on the circumstances? For instance interface IDog { name: string; weight: number; } class Retriever implements IDog { name = "Dug"; weight = 70; public updateAttribute(props ...

Ending subscription to an observable in Angular

Whenever I press a button, I receive information from a server about a specific vehicle by subscribing to an observable. If I press the same button again, I want to unsubscribe from the current "vehicleDetail" data that I'm viewing (to prevent memory ...

TypeScript is not compatible with the intended ECMAScript version

Out of the blue, it seems like my VS17 has started transpiling my TypeScript to ECMAScript 6, but now VS is not accepting it and throwing a bunch of SCRIPT1002 and SCRIPT1006 errors suddenly: "JavaScript critical error at line 3, column 5 in http://localho ...

Check that the elements within the array are present in the observable array

I need to confirm whether the items in the array below: const payment1: Payment = new Payment('1000'); // 1000 = id const payment2: Payment = new Payment('1001'); const paymentArray: Payment[]; paymentArray.push(payment1, payment2); ...

Step-by-step guide on mocking a method within a method using JEST

Currently, I am facing a challenge in unit testing a method named search. This method consists of three additional methods - buildSearchParameter, isUnknownFields, and readAll. I am looking to mock these methods but I am uncertain about the process. Can ...

Utilizing React Higher Order Components with called functions: "You cannot render functions as a React child"

Please refer to this codesandbox. I am currently working on creating a react Higher Order Component (HOC) that takes in both a functional component and a function to be executed within the HOC. In order to do this, I understand that I need to develop a re ...

What sets 'babel-plugin-module-resolver' apart from 'tsconfig-paths'?

After coming across a SSR demo (React+typescript+Next.js) that utilizes two plugins, I found myself wondering why exactly it needs both of them. In my opinion, these two plugins seem to serve the same purpose. Can anyone provide insight as to why this is? ...

Fixing the error "cannot call a function which is possibly undefined" in React and TypeScript

Seeking a resolution to the error "cannot invoke an object which can possibly be undefined" by utilizing react and typescript. What is the issue at hand? The problem arises when using the useContext react hook to create a variable (dialogContext) in compo ...

Conditioning types for uninitialized objects

Is there a way to create a conditional type that can determine if an object is empty? For instance: function test<T>(a: T): T extends {} ? string : never { return null } let o1: {} let o2: { fox? } let o3: { fox } test(o1) ...

Is there a specific directive in Angular that allows for variable declarations using the "

This interesting piece discusses the usage of a let-name directive in the template: <ng-template #testTemplate let-name> <div>User {{ name }} </div> </ng-template> Can anyone tell me if this is a part of the standard ang ...

Is there any advice for resolving the issue "In order to execute the serve command, you must be in an Angular project, but the project definition cannot be located"?

Are there any suggestions for resolving the error message "The serve command requires to be run in an Angular project, but a project definition could not be found."? PS C:\angular\pro\toitsu-view> ng serve The serve command requires to be ...

Typescript: Using axios to retrieve POST response beyond function boundaries

I've been working on a Typescript function that is supposed to generate and return a token value. Everything seems to be functioning properly, but I'm encountering an issue where the token value is only being logged to the console instead of bein ...

Managing Import Structure in Turborepo/Typescript Package

I am currently working on creating a range of TypeScript packages as part of a Turborepo project. Here is an example of how the import structure for these packages looks like: import { Test } from "package-name" import { Test } from "package ...

Error in NextJS Middleware Server: Invalid attempt to export a nullable value for "TextDecoderStream"

I've recently created a straightforward Next.js application using bun (version 1.0.4, bun create next-app), incorporating app routing with Next.js version 13.5.4 and a designated source directory. My goal was to implement a middleware that restricts a ...

Discover the dynamic method for determining the value of a nested array based on a specific condition

Currently, I am delving into the world of nested arrays and attempting to locate a specific value within the array based on a condition. The data returned by the API is structured in the following format: data= { "ch_Id": "1234", "title": "Title" ...

Encountering an unanticipated DOMException after transitioning to Angular 13

My Angular project is utilizing Bootstrap 4.6.2. One of the components features a table with ngb-accordion, which was functioning properly until I upgraded the project to Angular 13. Upon accessing the page containing the accordion in Angular 13, I encount ...

Fastify fails to register middleware when using the decorate method

In the process of crafting middleware for scrutinizing the JWT transmitted during route invocations, I meticulously adhered to the guidelines stipulated in the JWT middleware manual and ported the code to TypeScript from vanilla JS. Regrettably, an anomaly ...