Сопоставить ввод-вывод с массивом Either в fp-ts - PullRequest
1 голос
/ 12 марта 2020

Может кто-нибудь помочь мне понять, как это сделать в fp-ts?

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);

Я могу сделать все это с TaskEither, но я не знаю, как смешать это с IO или, может быть, я вообще не должен использовать IO?

Вот что я придумал:

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
  );
}

Обновление:

Я должен упомянуть и уточнить для меня наиболее сложную часть - как изменить набор с IO на массив Either, а затем отфильтровать или проигнорировать left s и продолжить с Task или TaskEither

Ошибка TypeScript 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)))),
  );

1 Ответ

3 голосов
/ 12 марта 2020

Если вы хотите сделать это «должным образом», то вам нужно обернуть все недетерминированные c (не чистые) вызовы функций в IO или IOEither (в зависимости от того, могут они или не могут выйти из строя).

Итак, сначала давайте определим, какие вызовы функций являются «чистыми», а какие нет. Самое простое, о чем я могу подумать, это так: если функция ВСЕГДА выдает одинаковый вывод для того же входа и не вызывает наблюдаемого side- эффекты, то это чисто.

«Тот же вывод» не означает ссылочное равенство, это означает структурное / поведенческое равенство. Поэтому, если ваша функция возвращает другую функцию, эта возвращаемая функция может быть не тем же функциональным объектом, но она должна вести себя одинаково (чтобы исходная функция считалась чистой).

Итак, в этих терминах следующее true:

  • cherio.load чисто
  • $ чисто
  • .get не чисто
  • .find не является чисто
  • .attr не чисто
  • .map чисто
  • .filter чисто

Теперь давайте создадим обертки для всех вызовы не-чистых функций:

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))

Стоит отметить, что здесь мы применяем не-чистую функцию (.attr или attrIO в упакованной версии) к массиву элементов. Если мы просто отобразим attrIO в массиве, мы вернемся Array<IO<result>>, но это не супер полезно, вместо этого мы хотим IO<Array<result>>. Для этого нам нужно traverse вместо map https://gcanti.github.io/fp-ts/modules/Traversable.ts.html.

Так что если у вас есть массив rows и вы хотите применить к нему attrIO, вы делаете это так:

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

const rows: Array<...> = ...;
// normal map
const mapped: Array<IO<...>> = rows.map(attrIO('data-test'));
// same result as above `mapped`, but in fp-ts way instead of native array map
const mappedFpTs: Array<IO<...>> = array.map(rows, attrIO('data-test')); 

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

Затем просто соберите все вместе:

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))
  );
}

Теперь getTests возвращает вам IO тех же элементов, которые были в вашей переменной tests в исходном коде.

Отказ от ответственности: я не запускал код через компилятор, возможно, он содержит некоторые опечатки или ошибки. Возможно, вам также понадобится приложить некоторые усилия, чтобы все это было строго напечатано.

EDIT :

Если вы хотите сохранить информацию об ошибках (в данном случае, пропущенных data-test (один из элементов a), у вас есть несколько вариантов сделать это. В настоящее время getTests возвращает IO<string[]>. Чтобы разместить там информацию об ошибках, вы можете сделать:

  • IO<Either<Error, string>[]> - IO, который возвращает массив, где каждый элемент является либо значением ошибки ИЛИ. Чтобы поработать с ним, вам все равно придется выполнить фильтрацию позже, чтобы избавиться от ошибок. Это наиболее гибкое решение, так как вы не теряете никакой информации, но оно также кажется бесполезным, потому что Either<Error, string> в этом случае почти такое же, как string | null.
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[]> - IO, который возвращает либо ошибку, либо массив значений. Здесь самое обычное, что нужно сделать, это вернуть Error, когда вы получите первый отсутствующий атрибут, и вернуть массив значений, если все значения не ошибочны. Итак, еще раз, это решение теряет информацию о правильных значениях, если есть какие-либо ошибки, и теряет информацию обо всех ошибках, кроме первой.
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[]> - IO, который возвращает либо массив ошибок ИЛИ массив значений. Это объединяет ошибки, если они есть, и объединяет значения, если ошибок нет. Это решение теряет информацию о правильных значениях, если есть какие-либо ошибки.

Этот подход на практике встречается реже, чем описанные выше, и его сложнее реализовать. Одним из распространенных вариантов использования является проверка достоверности, для которой есть монадный трансформатор https://gcanti.github.io/fp-ts/modules/ValidationT.ts.html. У меня нет особого опыта в этом, поэтому я не могу сказать больше об этом topi c.

  • IO<{ errors: Error[], values: string[] }> - IO, который возвращает объект, содержащий как ошибки, так и значения. Это решение также не теряет никакой информации, но его немного сложнее реализовать.

Канонический способ сделать это - определить экземпляр моноида для объекта результата { errors: Error[], values: string[] }, а затем агрегировать результаты с использованием foldMap:

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))
  );
}
...