Transform the IO type to an array of Either in functional programming with the fp-ts

Looking for assistance with implementing this in fp-ts. Can someone provide guidance?

const $ = cheerio.load('some text');
const tests = $('table tr').get()
  .map(row => $(row).find('a'))
  .map(link => link.attr('data-test') ? link.attr('data-test') : null)
  .filter(v => v != null);

I've managed to work with TaskEither, but I'm unsure how to incorporate IO or if it's necessary. Any suggestions on this front would be appreciated.

Here is what I have so far:

const selectr = (a: CheerioStatic): CheerioSelector => (s: any, c?: any, r?: any) => a(s, c, r);

const getElementText = (text: string) => {
  return pipe(
    IO.of(cheerio.load),
    IO.ap(IO.of(text)),
    IO.map(selectr),
    IO.map(x => x('table tr')),
    // ?? don't know what to do here
  );
}

Update:

The most challenging part for me is transforming the typings from IO to an array of Either, then filtering out or ignoring the lefts and proceeding with Task or TaskEither.

The TypeScript error I encounter is

Type 'Either<Error, string[]>' is not assignable to type 'IO<unknown>'

const getAttr = (attrName: string) => (el: Cheerio): Either<Error, string> => {
  const value = el.attr(attrName);
  return value ? Either.right(value) : Either.left(new Error('Empty attribute!'));
}

const getTests = (text: string) => {
  const $ = cheerio.load(text);
  return pipe(
    $('table tbody'),
    getIO,
    // How to go from IO<string> to IOEither<unknown, string[]> or something similar?
    // What happens to the array of errors do we keep them or we just change the typings?
    IO.chain(rows => A.array.traverse(E.either)(rows, flow($, attrIO('data-test)))),
  );

Answer №1

If you aim to follow best practices, it is essential to encapsulate all non-deterministic (non-pure) function calls within IO or IOEither (depending on their potential failure).

To distinguish between "pure" and non-pure functions, consider this simple rule - if a function consistently produces the same output for the same input without causing any observable side effects, it is deemed pure.

The definition of "same output" does not imply referential equality but rather structural/behavioral equivalence. Therefore, even if a function returns another function that may not be the exact object, it should exhibit identical behavior for the original function to maintain purity.

Based on these criteria, the following classification holds true:

  • cherio.load is a pure function
  • $ is a pure function
  • .get is not pure
  • .find is not pure
  • .attr is not pure
  • .map is pure
  • .filter is pure

To address non-pure function calls, let's create wrappers as follows:

const getIO = selection => IO.of(selection.get())
const findIO = (...args) => selection => IO.of(selection.find(...args))
const attrIO = (...args) => element => IO.of(element.attr(...args))

It's important to note that when applying a non-pure function like .attr or attrIO to an array of elements, mapping attrIO directly would yield Array<IO<result>>, which can be enhanced by using traverse instead of map.

In the case where you want to apply attrIO to an array named

rows</code, here's how you can do it:</p>

<pre><code>import { array } from 'fp-ts/lib/Array';
import { io } from 'fp-ts/lib/IO';

const rows: Array<...> = ...;
// regular map
const mapped: Array<IO<...>> = rows.map(attrIO('data-test'));
// equivalent to above with fp-ts functionality
const mappedFpTs: Array<IO<...>> = array.map(rows, attrIO('data-test')); 

// leveraging traverse instead of map to align `IO` with `Array` in the type signature
const result: IO<Array<...>> = array.traverse(io)(rows, attrIO('data-test'));

Finally, let's put everything together:

import { array } from 'fp-ts/lib/Array';
import { io } from 'fp-ts/lib/IO';
import { flow } from 'fp-ts/lib/function';

const getIO = selection => IO.of(selection.get())
const findIO = (...args) => selection => IO.of(selection.find(...args))
const attrIO = (...args) => element => IO.of(element.attr(...args))

const getTests = (text: string) => {
  const $ = cheerio.load(text);
  return pipe(
    $('table tr'),
    getIO,
    IO.chain(rows => array.traverse(io)(rows, flow($, findIO('a')))),
    IO.chain(links => array.traverse(io)(links, flow(
      attrIO('data-test'), 
      IO.map(a => a ? a : null)
    ))),
    IO.map(links => links.filter(v => v != null))
  );
}

With the updated getTests, you receive back an IO containing the same elements from your initial tests variable.

EDIT:

If you wish to retain error information, such as missing data-test attributes on certain a elements, there are several approaches available. Currently, getTests yields an IO<string[]>. To accommodate error details, consider the following options:

  • IO<Either<Error, string>[]>
    - an IO returning an array where each element signifies either an error or a value. This approach maintains maximum flexibility but may seem redundant since Either<Error, string> is akin to string | null in this context.
import * as Either from 'fp-ts/lib/Either';

const attrIO = (...args) => element: IO<Either<Error, string>> => IO.of(Either.fromNullable(new Error("not found"))(element.attr(...args) ? element.attr(...args): null));

const getTests = (text: string): IO<Either<Error, string>[]> => {
  const $ = cheerio.load(text);
  return pipe(
    $('table tr'),
    getIO,
    IO.chain(rows => array.traverse(io)(rows, flow($, findIO('a')))),
    IO.chain(links => array.traverse(io)(links, attrIO('data-test')))
  );
}
  • IOEither<Error, string[]> - an IO delivering either an error or an array of values. Typically, errors are returned upon encountering the first missing attribute, failing to capture other errors or valid values beyond that point.
import * as Either from 'fp-ts/lib/Either';
import * as IOEither from 'fp-ts/lib/IOEither';

const { ioEither } = IOEither;

const attrIOEither = (...args) => element: IOEither<Error, string> => IOEither.fromEither(Either.fromNullable(new Error("not found"))(element.attr(...args) ? element.attr(...args): null));

const getTests = (text: string): IOEither<Error, string[]> => {
  const $ = cheerio.load(text);
  return pipe(
    $('table tr'),
    getIO,
    IO.chain(rows => array.traverse(io)(rows, flow($, findIO('a')))),
    IOEither.rightIO, // "lift" IO to IOEither context
    IOEither.chain(links => array.traverse(ioEither)(links, attrIOEither('data-test')))
  );
}
  • IOEither<Error[], string[]>
    - an IO providing either an array of errors or an array of values. This method aggregates errors while preserving multiple value results. Nevertheless, in the presence of errors, correct values remain undocumented.

This technique, although less common, proves intricate to implement and primarily suitable for validation checks. A possible solution involves employing a monad transformer like . More insights on its practical implications could shed light on better use cases.

  • IO<{ errors: Error[], values: string[] }>
    - an IO outcome comprising both error logs and value arrays. While comprehensive, this scenario poses challenges regarding implementation complexity.

A recommended practice involves defining a monoid instance for the resultant object

{ errors: Error[], values: string[] }
and subsequent utilization of foldMap to combine outcomes:

import { Monoid } from 'fp-ts/lib/Monoid';

type Result = { errors: Error[], values: string[] };

const resultMonoid: Monoid<Result> = {
  empty: {
    errors: [],
    values: []
  },
  concat(a, b) {
    return {
      errors: [].concat(a.errors, b.errors),
      values: [].concat(a.values, b.values)
    };
  } 
};

const attrIO = (...args) => element: IO<Result> => {
  const value = element.attr(...args);
  if (value) {
    return {
      errors: [],
      values: [value]
    };
  } else {
    return {
      errors: [new Error('not found')],
      values: []
  };
};

const getTests = (text: string): IO<Result> => {
  const $ = cheerio.load(text);
  return pipe(
    $('table tr'),
    getIO,
    IO.chain(rows => array.traverse(io)(rows, flow($, findIO('a')))),
    IO.chain(links => array.traverse(io)(links, attrIO('data-test'))),
    IO.map(results => array.foldMap(resultMonoid)(results, x => x))
  );
}

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

Why do callbacks in Typescript fail to compile when their arguments don't match?

In my current project, I encountered a scenario where a React callback led to a contrived example. interface A { a: string b: string } interface B { a: string b: string c: string } function foo(fn: (a: A) => void, a: A) { fn( ...

Accessing instance variables from a chained observable function in Angular 2/Typescript

Currently, I am utilizing Angular2 along with Typescript. Let's assume that there is a dummy login component and an authentication service responsible for token authentication. In one of the map functions, I intend to set the variable authenticated as ...

Establish a connection between MongoDB and the built-in API in Next.js

I've been working on integrating a MongoDB database with the Next.js built-in API by using the code snippet below, which I found online. /api/blogs/[slug].ts import type { NextApiRequest, NextApiResponse } from 'next' import { connectToData ...

Troubleshooting problems with permissions when using the AWS IAM assumeRole function with session

Within my aws-cdk application, I am working on setting up a lambda function to assume a specific role and obtain credentials through aws-sts. These credentials need to include a tenant_id tag. AWS CDK Code: Role to be assumed: const pdfUserRole = new Rol ...

What could be causing TypeScript to not locate my custom package?

I decided to create a fork of an existing package and released it with a new, distinct name: https://www.npmjs.com/package/feed-media-fork After tagging a new version, setting up a release on GitHub, and running yarn add feed-media-fork or the equivalent ...

Unable to transfer variable from a function to the test in Protractor

Currently, I am working on a test to verify the amount of gold in my possession. The test is being conducted using TypeScript and Protractor. Within this testing scenario, I have a method named GetAmountOfChips: public static GetAmountOfChips(): PromiseL ...

What is the correct way to import scss files in a Next.js project?

Currently, I am working on a NextJS project that uses Sass with TypeScript. Everything is running smoothly in the development environment, but as soon as I attempt to create a build version of the project, I encounter this error. https://i.stack.imgur.com ...

Utilizing the onscroll feature to trigger a function within a ScrollView

I am facing an issue with an animated view where I need to execute multiple events within the onScroll property. The current onScroll implementation is as follows: onScroll={ Animated.event( [{ nativeEvent: { conten ...

Tips for modifying the language of an Angular Application's OneTrust Cookie Banner

I'm currently developing an Angular application and utilizing OneTrust for managing cookie consent. The issue I'm encountering is that while the rest of the components on the login page are properly translated into the target language, the OneTru ...

Angular: utilizing input type="date" to set a default value

Looking for a way to filter data by date range using two input fields of type "date"? I need these inputs to already display specific values when the page loads. The first input should have a value that is seven days prior to today's date, while the ...

What is the best method for managing an event loop during nested or recursive calculations?

When it comes to breaking a computation and releasing using setTimeout(), most examples seen involve having a shallow call stack. But what about scenarios where the computation is deeply nested or mutually-recursive, like in a tree search, with plenty of c ...

One important rule to remember when using React Hooks is that they must be called consistently and in the correct order in each render of the component. It

Encountering an issue while trying to use the ternary operator to determine which React Hook to utilize. The error message states: "React Hook "useIsolationDynamicPhase" is called conditionally. React Hooks must be invoked in the same order during every co ...

Getting the specific nested array of objects element using filter in Angular - demystified!

I've been attempting to filter the nested array of objects and showcase the details when the min_age_limit===18. The JSON data is as follows: "centers": [ { "center_id": 603425, "name" ...

Encountering Duplicate Identifier Error while working on Angular 2 Typescript in Visual Studio Code

Currently attempting to configure a component in Angular 2 with Typescript using Visual Studio Code on Mac. Encounter the following errors when trying the code below: duplicate identifier 'Component'. and Duplicate identifier' DashboardCompo ...

Exploring Angular14: A guide to efficiently looping through the controls of strictly typed FormGroups

Currently, I am working on upgrading my formGroups to be strictly typed in Angular v14. Within my FormGroup, there is a specific block of logic that iterates through all the controls and performs an action (this part is not crucial as I am facing issues be ...

How can I upload multiple images in one request using Typescript?

HTML: <div> <input type ="file" (change)="selectFiles($event)" multiple="multiple" /> </div> Function to handle the change event selectFiles(event) { const reader = new FileReader(); if (event.target.files & ...

The usage of @Inject('Window') in Angular/Jasmine leads to test failures, but removing it results in code failures

Currently, I am encountering a dilemma related to using Angular's @Inject('Window') within a web API service. This particular issue arises when the Window injection is utilized in the service constructor, leading to test spec failures in the ...

The NgModel within the parent component is expected to mirror the state of the child component

My Angular 10 project includes a library with a wrapper component around a primeng-component. The primeng-component utilizes ngModel. I am trying to set the ngModel in the parent-component accessing the wrapper-component, and I want any changes made to the ...

What is the best way to set State conditionally based on two different objects, each with its own type, without resorting to

I am trying to create two different objects, each with slightly different types, in order for them to be compatible with a state object that contains values of both types. However, I am encountering the following error: Property 'dataA' does no ...

Troubleshooting Angular 7 Build Failure: Missing Typescript .d.ts Declaration Files for Ahead-of-Time Compilation

I have encountered difficulties in building my Angular 7 application after upgrading from v6. When I use ng build, everything runs smoothly. However, when I try either ng serve --aot, ng build --aot, or ng build --prod (which also includes aot), an error ...