【问题标题】:Map IO to array of Either in fp-ts将 IO 映射到 fp-ts 中的 Either 数组
【发布时间】:2020-03-11 23:22:22
【问题描述】:

谁能帮我弄清楚如何在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 的数组,然后过滤或忽略 lefts 并继续使用 TaskTaskEither

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

【问题讨论】:

  • 你不能用一个选择器来做吗?类似$('table tr a[data-test]')

标签: typescript fp-ts


【解决方案1】:

如果您想“正确”地执行此操作,则需要将所有非确定性(非纯)函数调用包装在 IO 或 IOEither 中(取决于它们是否会失败)。

首先让我们定义哪些函数调用是“纯”的,哪些不是。我发现最容易想到的就是这样 - 如果函数总是为 same input 提供 same output 并且不会导致任何 observable 副作用,那就是纯粹的。

“相同的输出”并不意味着引用平等,它意味着结构/行为平等。所以如果你的函数返回另一个函数,这个返回的函数可能不是同一个函数对象,但它的行为必须相同(原始函数被认为是纯函数)。

所以在这些方面,以下是正确的:

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

需要注意的一点是,我们在这里对元素数组应用了非纯函数(.attrattrIO 在包装版本中)。如果我们只在数组上映射attrIO,我们会返回Array&lt;IO&lt;result&gt;&gt;,但它不是超级有用,我们需要IO&lt;Array&lt;result&gt;&gt;。为此,我们需要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 会返回一个与原始代码中tests 变量中相同元素的 IO。

免责声明:我没有通过编译器运行代码,它可能有一些拼写错误或错误。您可能还需要付出一些努力来使其全部成为强类型。

编辑

如果您想保留有关错误的信息(在这种情况下,缺少 a 元素之一上的 data-test 属性),您有多种选择。目前getTests 返回IO&lt;string[]&gt;。要在此处放置错误信息,您可以这样做:

  • IO&lt;Either&lt;Error, string&gt;[]&gt; - 一个返回数组的 IO,其中每个元素都是错误或值。要使用它,您仍然需要稍后进行过滤以消除错误。这是最灵活的解决方案,因为您不会丢失任何信息,但感觉也有点没用,因为在这种情况下 Either&lt;Error, string&gt;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&lt;Error, string[]&gt; - 一个返回错误或值数组的 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&lt;Error[], string[]&gt; - 一个返回错误数组或值数组的 IO。如果有任何错误,这个会聚合错误,如果没有错误,则会聚合值。如果有任何错误,此解决方案会丢失有关正确值的信息。

这种方法在实践中比上述方法更罕见,实施起来也更棘手。一个常见的用例是验证检查,为此有一个单子转换器https://gcanti.github.io/fp-ts/modules/ValidationT.ts.html。我没有太多经验,所以不能就这个话题多说。

  • IO&lt;{ errors: Error[], values: string[] }&gt; - 一个返回包含错误和值的对象的 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))
  );
}

【讨论】:

  • 很好,感谢您的回答。我很好奇的另一件事是IOEither,实际上我认为我应该使用它来跳过代码的.filter() 部分。你会改进你的答案来涵盖那部分吗?
  • 我认为如果我们修改 attrIO 以返回 IOEither 将完成这项工作,我只是不知道如何将类型改回 IO
  • 我更新了这部分问题。请看一看。谢谢
  • 这太有趣了!非常感谢您花时间解释多种方法。
猜你喜欢
  • 2021-05-10
  • 2022-09-25
  • 2020-03-26
  • 1970-01-01
  • 2020-08-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-07-17
相关资源
最近更新 更多