Exploring the depths of nested data retrieval using the fp-ts library: a labyrinth

Embark on your journey into the world of functional programming in typescript using the fp-ts library.

I find myself tangled in a complex web of nested data fetching, reminiscent of the ancient Egyptian pyramids. How can I tackle this problem with a more streamlined and less imperative approach?

The Challenge

export const getProgramWithAllElements = (programId: FirestoreDocumentId): TE.TaskEither<FirestoreError, Program> =>
  pipe(
    getProgram(programId),
    TE.chain((program) =>
      pipe(
        getCollCollectionsFromPath(program.id),
        TE.chain((collections) =>
          pipe(
            collections,
            A.map((collection) =>
              pipe(
                getWeekCollectionsFromPath(program.id)(collection.id),
                TE.chain((weeks) =>
                  pipe(
                    weeks,
                    A.map((week) =>
                      pipe(
                        getDayCollectionsFromPath(program.id)(collection.id)(week.id),
                        TE.chain((days) =>
                          pipe(
                            days,
                            A.map((day) =>
                              pipe(
                                getExerciseCollectionsFromPath(program.id)(collection.id)(week.id)(day.id),
                                TE.map(
                                  (exercises) =>
                                    ({
                                      ...day,
                                      exercises: exercises,
                                    } as Day)
                                )
                              )
                            ),
                            A.sequence(TE.taskEither)
                          )
                        ),
                        TE.map(
                          (days) =>
                            ({
                              ...week,
                              days: days,
                            } as Week)
                        )
                      )
                    ),
                    A.sequence(TE.taskEither)
                  )
                ),
                TE.map(
                  (weeks) =>
                    ({
                      ...collection,
                      weeks: weeks,
                    } as Collection)
                )
              )
            ),
            A.sequence(TE.taskEither)
          )
        ),
        TE.map(
          (collections) =>
            ({
              ...program,
              collections: collections,
            } as Program)
        )
      )
    )
  );

Methods utilized in script

declare const getProgram: (programId: FirestoreDocumentId) => TE.TaskEither<FirestoreError, Program>;

declare const getCollCollectionsFromPath: (
  programId: FirestoreDocumentId
) => TE.TaskEither<FirestoreError, Collection[]>;

declare const getWeekCollectionsFromPath: (
  programId: FirestoreDocumentId
) => (collectionId: FirestoreDocumentId) => TE.TaskEither<FirestoreError, Week[]>;

declare const getDayCollectionsFromPath: (programId: FirestoreDocumentId) => (collectionId: FirestoreDocumentId) => (
  weekId: FirestoreDocumentId
) => TE.TaskEither<FirestoreError, Day[]>;

declare const getExerciseCollectionsFromPath: (programId: FirestoreDocumentId) => (collectionId: FirestoreDocumentId) => (
  weekId: FirestoreDocumentId
) => (dayId: FirestoreDocumentId) => TE.TaskEither<FirestoreError, Exercise[]>;

Refined Data Structure

export interface Program {
    id: string;
    // Other Fields
    collections?: Collection[];
}

export interface Collection {
    id: string;
    // Other Fields
    weeks?: Week[];
}

export interface Week {
    id: string;
    // Other Fields
    days?: Day[];
}

export interface Day {
    id: string;
    // Other Fields
    exercises: ProgramExercise[];
}

export interface ProgramExercise {
    id: string;
    // Other Fields
}

Answer №1

While I'm not familiar with FP-TS, I can offer a broad response that highlights the consistent algebraic principles at play.

To begin, monads naturally create nested structures. Although this is unavoidable, you can conceal it if equipped with the appropriate tool (such as the `do` notation in Haskell). Regrettably, in Javascript, evading nesting altogether isn't feasible generally; however, by using generators, you can sustain a flat structure for deterministic computations. The code provided below relies on the scriptum lib, which I curate:

const record = (type, o) => (
  o[Symbol.toStringTag] = type.name || type, o);

const Cont = k => record(Cont, {cont: k});

const contOf = x => Cont(k => k(x));

const contChain = mx => fm =>
  Cont(k =>
    mx.cont(x =>
      fm(x).cont(k)));

const _do = ({chain, of}) => init => gtor => {
  const go = ({done, value: x}) =>
    done
      ? of(x)
      : chain(x) (y => go(it.next(y)));

  const it = gtor(init);
  return go(it.next());
};

const log = (...ss) =>
  (console.log(...ss), ss[ss.length - 1]);

const inck = x => Cont(k => k(x + 1));

const sqrk = x => Cont(k =>
  Promise.resolve(null)
    .then(k(x * x)));

const mainM = _do({chain: contChain, of: contOf}) (5) (function* (init) {
  const x = yield inck(init),
    y = yield sqrk(init);

  return [x, y];
});

mainM.cont(log)

However, based on your code snippet, it appears that you may not necessarily require a monad since subsequent computations do not rely on previous values, akin to

chain(tx) (x => x === 0 ? of(x) : of(2/x)
. An Applicative functor should suffice:

const record = (type, o) => (
  o[Symbol.toStringTag] = type.name || type, o);

const Cont = k => record(Cont, {cont: k});

const contMap = f => tx =>
  Cont(k => tx.cont(x => k(f(x))));

const contAp = tf => tx =>
  Cont(k =>
    tf.cont(f =>
      tx.cont(x =>
        k(f(x)))));

const contOf = x => Cont(k => k(x));

const liftA2 = ({map, ap}) => f => tx => ty =>
  ap(map(f) (tx)) (ty);

const contLiftA2 = liftA2({map: contMap, ap: contAp});

const log = (...ss) =>
  (console.log(...ss), ss[ss.length - 1]);

const inck = x => Cont(k => k(x + 1));

const sqrk = x => Cont(k =>
  Promise.resolve(null)
    .then(k(x * x)));

const mainA = contLiftA2(x => y => [x, y]) (inck(5)) (sqrk(5))

mainA.cont(log);

It's essential to note that incorporating generators with non-deterministic computations like linked lists or arrays poses a challenge. Nonetheless, leveraging monads applicatively can alleviate the complication:

const arrChain = mx => fm =>
  mx.flatMap(fm);

const chain2 = chain => mx => my => fm =>
  chain(chain(mx) (x => fm(x)))
    (gm => chain(my) (y => gm(y)));

const log = (...ss) =>
  (console.log(...ss), ss[ss.length - 1]);

const xs = [1,2],
  ys = [3,4];

main3 = chain2(arrChain) (xs) (ys) (x => [y => [x, y]]);

log(main3);

Although a nested structure remains evident, the overall presentation looks more organized and clean.

I cannot be entirely certain if this method is universally applicable to all monads, as it executes effects twice as frequently as usual. Thus far, no issues have emerged, instilling confidence in its effectiveness.

Answer №2

Attempting to abstract and simplify repetitive patterns:

import * as B from "fp-ts/Array";
import { compose, pipe } from "fp-ts/lib/function";

type DocumentId = number;
interface Collection {
  id: number;
}
interface Error {
  id: number;
}
interface Day {
  id: number;
}
interface Week {
  id: number;
}
interface Program {
  id: number;
}
interface Exercise {
  id: number;
}

declare const fetchProgram: (
  programId: DocumentId
) => TE.TaskEither<Error, Program>;

declare const fetchCollectionsAtPath: (
  programId: DocumentId
) => TE.TaskEither<Error, Collection[]>;

declare const fetchWeeksAtPath: (
  programId: DocumentId
) => (
  collectionId: DocumentId
) => TE.TaskEither<Error, Week[]>;

declare const fetchDaysAtPath: (
  programId: DocumentId
) => (
  collectionId: DocumentId
) => (weekId: DocumentId) => TE.TaskEither<Error, Day[]>;

declare const fetchExercisesAtPath: (
  programId: DocumentId
) => (
  collectionId: DocumentId
) => (
  weekId: DocumentId
) => (dayId: DocumentId) => TE.TaskEither<Error, Exercise[]>;

const assignTo = <K extends string>(prop: K) => <T>(obj: T) => <E, A>(
  task: TE.TaskEither<E, A>
) =>
  pipe(
    task,
    TE.map(
      (data): T & { [P in K]: A } =>
        Object.assign({}, obj, { [prop]: data }) as any
    )
  );

const sequenceChained = <E, A, B>(f: (t: A) => TE.TaskEither<E, B>) =>
  TE.chain(compose(B.map(f), B.sequence(TE.taskEither)));

const chainSequenceAndAssignTo = <K extends string>(prop: K) => <E, B>(
  f: (id: number) => TE.TaskEither<E, B[]>
) => <A extends { id: number }>(
  task: TE.TaskEither<E, A[]>
) =>
  pipe(
    task,
    sequenceChained((a) => pipe(f(a.id), assignTo(prop)(a)))
  );

export const fetchProgramWithElements = (programId: DocumentId) =>
  pipe(
    fetchProgram(programId),
    TE.chain((program) =>
      pipe(
        fetchCollectionsAtPath(program.id),
        chainSequenceAndAssignTo("collections")((collectionId) =>
          pipe(
            fetchWeeksAtPath(program.id)(collectionId),
            chainSequenceAndAssignTo("weeks")((weekId) =>
              pipe(
                fetchDaysAtPath(program.id)(collectionId)(weekId),
                chainSequenceAndAssignTo("exercises")(
                  fetchExercisesAtPath(program.id)(collectionId)(
                    weekId
                  )
                )
              )
            )
          )
        )
      )
    )
  );

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

Angular 2: Dynamically Adjusting View Components Based on URL Path

Apologies for the unconventional title. I struggled to come up with a better one. My goal is to develop an application with a simple 3-part structure (header / content / footer). The header should change based on the active route, where each header is a s ...

All-encompassing NextJS App router with a focus on Internationalization

I am currently working with a folder structure that includes an optional catch-all feature. The issue I am facing is that the page does not change when the URL is altered to include ""/"" or ""/about-us"". It consistently remains on the ...

The data in the Angular variable is not persisting

After calling this function to retrieve an array of Articles, I noticed that the data is not being saved as expected. Take a look at my console output below. GetAll() { //return this.http.get<Array<Article>>(this.cfg.SERVER); this.http.get ...

What could be causing the "Failed to compile" error to occur following the execution of npm

Exploring react with typescript, I created this simple and basic component. import React, { useEffect, useState } from "react"; import "./App.css"; type AuthUser = { name: string; email: string; }; function App() { const [user, setUser] = useState& ...

Utilizing ngModel within a nested ngFor loop in Angular to capture changing values dynamically

I have been working on developing a screen using Angular and I'm facing an issue with binding values using ngModel. https://i.sstatic.net/DCJ3T.png Here is my implementation. Any help would be appreciated. The model entity I am using for binding the ...

"Exploring the world of 3rd party libraries in Angular2 with Typescript and Webpack

I've begun working on a fantastic seed project that can be found at: https://github.com/AngularClass/angular2-webpack-starter However, I've encountered an issue with integrating third-party modules. Can anyone offer guidance on how to properly a ...

Errors related to reducer types in createSlice of Redux Toolkit

As I embark on a new React-Redux project with Typescript, I find myself facing some challenges despite my previous experience. While my knowledge of React and Redux is solid, I am still getting acquainted with Redux toolkit. Transitioning from a typed back ...

Querying data conditionally with Angular rxjs

I have a json file that contains multiple arrays structured like this: { A[] B[] C[] ... } This is the query I am using: myFunction():void{ this.apiService.getData() .pipe( map((response: any) => response.A), // to access to the &ap ...

Transforming a detailed JSON structure into a more simplified format with Angular 2

As a newcomer to Angular 2, I find myself encountering a hurdle in filtering unnecessary data from a JSON object that is retrieved from a REST API. Below is an example of the JSON data I am working with: { "refDataId":{ "rdk":1, "refDataTy ...

What is the best way to create TypeScript declarations for both commonjs modules and global variables?

Wanting to make my TypeScript project compatible with both the commonjs module system and globals without modules. I'm considering using webpack for bundling and publishing it into the global namespace, but running into issues with the definitions (.d ...

Trouble with Excel Office Script setInterval functionality

Trying to automatically recalculate an Excel worksheet every second using Office Script. Unfortunately, my initial approach did not succeed. function sendUpdate(sheet: ExcelScript.Worksheet) { console.log('hi'); sheet.calculate(true); } func ...

Troubleshooting Puppeteer compatibility issues when using TypeScript and esModuleInterop

When attempting to use puppeteer with TypeScript and setting esModuleInterop=true in tsconfig.json, an error occurs stating puppeteer.launch is not a function If I try to import puppeteer using import * as puppeteer from "puppeteer" My questi ...

Unsynchronized state of affairs in the context of Angular navigation

Within my Angular project, I am currently relying on an asynchronous function called foo(): Promise<boolean>. Depending on the result of this function, I need to decide whether to display component Foo or Bar. Considering my specific need, what woul ...

Differences between Typescript Import and JavaScript import

/module/c.js, attempting to export name and age. export const name = 'string1'; export const age = 43; In b.ts, I'm trying to import the variables name and age from this .ts file import { name, age } from "./module/c"; console.log(name, ...

When utilizing Jest, the issue arises that `uuid` is not recognized as

My current setup is as follows: // uuid-handler.ts import { v4 as uuidV4 } from 'uuid'; const generateUuid: () => string = uuidV4; export { generateUuid }; // uuid-handler.spec.ts import { generateUuid } from './uuid-handler'; de ...

Expanding a TypeScript type by creating an alias for a property

I am working on defining a type that allows its properties to be "aliased" with another name. type TTitle: string; type Data<SomethingHere> = { id: string, title: TTitle, owner: TPerson, } type ExtendedData = Data<{cardTitle: "title&qu ...

Error encountered in Snap SVG combined with Typescript and Webpack: "Encountered the error 'Cannot read property 'on' of undefined'"

I am currently working on an Angular 4 app that utilizes Snap SVG, but I keep encountering the frustrating webpack issue "Cannot read property 'on' of undefined". One solution I found is to use snapsvg-cjs, however, this means losing out on the ...

Initial 16 characters of the deciphered message are nonsensical

In a specific scenario, I encounter data encryption from the API followed by decryption in TypeScript. I have utilized CryptoJS for decryption in TypeScript. Below is the decryption code snippet: decrypt(source: string, iv: string): string { var key = envi ...

How can I integrate keydown.control with a unique click function in Angular?

Is there a way to choose multiple number elements in random order and save them to an array by holding down the control key (CTRL) and clicking on the element? For example, selecting 2 and 4 out of 5. I tried different methods but couldn't figure out ...

Guide on accessing device details with Angular2 and Typescript

I'm working on populating a table with details using TypeScript, but I need some assistance. 1. Device Name 2. Device OS 3. Location 4. Browser 5. IsActive I'm looking for a solution to populate these fields from TypeScript. Can anyone lend me ...