简体   繁体   English

将 IO 映射到 fp-ts 中的任一数组

[英]Map IO to array of Either in fp-ts

Can anyone help me to figure out how to do this in fp-ts ?谁能帮我弄清楚如何在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);

I can do it all with TaskEither but I don't know how to mix it with IO , or maybe I shouldn't use IO at all?我可以用TaskEither完成这一切,但我不知道如何将它与IO混合,或者我根本不应该使用IO

This is what I came up with 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:更新:

I must mention and clarify the most challenging part for me is how to change typings from IO to an array of Either and then filter or ignore the left s and continue with Task or TaskEither不得不提的是,澄清最具挑战性的部分,我是如何从改变分型IO至数组Either ,然后过滤或忽略left S和继续TaskTaskEither

TypeScript error is Type 'Either<Error, string[]>' is not assignable to type 'IO<unknown>' 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)))),
  );

If you want to do it "properly", then you need to wrap all non-deterministic (non-pure) function calls in IO or IOEither (depending on whether they can or cannot fail).如果您想“正确”执行此操作,则需要将所有非确定性(非纯)函数调用包装在 IO 或 IOEither 中(取决于它们是否可以失败)。

So first let's define which function calls are "pure" and which are not.所以首先让我们定义哪些函数调用是“纯”的,哪些不是。 The easiest I find to think of it is like so - if function ALWAYS gives the same output for the same input and doesn't cause any observable side-effects, then it's pure.我发现最容易想到的就是这样 - 如果函数总是为相同的输入提供相同的输出并且不会引起任何可观察到的副作用,那么它就是纯粹的。

"Same output" doesn't mean referential equality, it means structural/behaviour equality. “相同的输出”并不意味着引用平等,而是意味着结构/行为平等。 So if your function returns another function, this returned function might not be the same function object, but it must behave the same (for the original function to be considered pure).所以如果你的函数返回另一个函数,这个返回的函数可能不是同一个函数对象,但它的行为必须相同(为了将原始函数视为纯函数)。

So in these terms, the following is true:所以在这些方面,以下是正确的:

  • cherio.load is pure cherio.load是纯的
  • $ is pure $是纯的
  • .get is not pure .get不纯
  • .find is not pure .find不纯
  • .attr is not pure .attr不纯
  • .map is pure .map是纯的
  • .filter is pure .filter是纯的

Now let's create wrappers for all non-pure function calls:现在让我们为所有非纯函数调用创建包装器:

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

One thing to note is that here we apply non-pure function ( .attr or attrIO in a wrapped version) on an array of elements.需要注意的一件事是,这里我们在元素数组上应用非纯函数(包装版本中的.attrattrIO )。 If we just map attrIO on the array, we get back Array<IO<result>> , but it's not super useful, we want IO<Array<result>> instead.如果我们只是将attrIO映射到数组上,我们会返回Array<IO<result>> ,但这不是很有用,我们想要IO<Array<result>>代替。 To achieve this, we need traverse instead of map https://gcanti.github.io/fp-ts/modules/Traversable.ts.html .为了实现这一点,我们需要traverse而不是map https://gcanti.github.io/fp-ts/modules/Traversable.ts.html

So if you have an array rows and you want to apply attrIO on it, you do it like so:所以如果你有一个数组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'));

Then just assemble 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))
  );
}

Now getTests gives you back an IO of same elements that were in your tests variable in original code.现在getTests为您返回原始代码中tests变量中相同元素的 IO。

Disclaimer: I haven't run the code through the compiler, it might have some typos or mistakes.免责声明:我没有通过编译器运行代码,它可能有一些拼写错误或错误。 You probably also need to put some effort to make it all strongly typed.您可能还需要付出一些努力来使其全部为强类型。

EDIT :编辑

If you want to preserve information on the errors (in this case, missing data-test attribute on one of the a elements), you have several options to do so.如果您想保留有关错误的信息(在这种情况下,缺少a元素之一上的data-test属性),您有多种选择。 Currently getTests returns an IO<string[]> .当前getTests返回一个IO<string[]> To fit error info there, you could do:为了适应那里的错误信息,你可以这样做:

  • IO<Either<Error, string>[]> - an IO that returns an array where each element is either error OR value. IO<Either<Error, string>[]> - 一个 IO,它返回一个数组,其中每个元素都是错误或值。 To work with it, you still need to do filtering later to get rid of the errors.要使用它,您仍然需要稍后进行过滤以消除错误。 This is the most flexible solution as you don't lose any information, but it feels kinda useless too because Either<Error, string> is pretty much the same in this case as string | null这是最灵活的解决方案,因为您不会丢失任何信息,但感觉也没什么用,因为在这种情况下, Either<Error, string>string | null几乎相同。 string | null . 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[]> - an IO that returns either an error OR an array of values. IOEither<Error, string[]> - 返回错误或值数组的 IO。 Here the most usual thing to do is to return Error when you get a first missing attribute, and return an array of values if all values are non-erroneous.这里最常见的做法是当你得到第一个缺失的属性时返回 Error ,如果所有值都没有错误,则返回一个值数组。 So again, this solution loses info about correct values if there are any errors AND it loses info about all errors except the first one.因此,如果有任何错误,此解决方案将丢失有关正确值的信息,并且会丢失有关除第一个错误之外的所有错误的信息。
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 that returns either an array of errors OR an array of values. IOEither<Error[], string[]> - 返回错误数组或值数组的 IO。 This one aggregates the errors if there are any, and aggregates the values if there are no errors.如果有任何错误,则聚合该错误,如果没有错误则聚合值。 This solution loses info about correct values if there are any errors.如果有任何错误,此解决方案将丢失有关正确值的信息。

This approach is more rare in practice than the above ones and is more tricky to implement.这种方法在实践中比上述方法更罕见,而且实现起来也更棘手。 One common use-case is validation check, and for that there is a monad transformer https://gcanti.github.io/fp-ts/modules/ValidationT.ts.html .一个常见的用例是验证检查,为此有一个 monad 转换器https://gcanti.github.io/fp-ts/modules/ValidationT.ts.html I don't have much experience with it, so can't say more on this topic.我没有太多的经验,所以不能在这个话题上多说。

  • IO<{ errors: Error[], values: string[] }> - an IO that returns an object containing both errors and values. IO<{ errors: Error[], values: string[] }> - 返回包含错误和值的对象的 IO。 This solution also doesn't lose any info, but is slightly more tricky to implement.这个解决方案也不会丢失任何信息,但实现起来稍微有点棘手。

The canonical way of doing it is to define a monoid instance for the result object { errors: Error[], values: string[] } and then aggregate the results using foldMap :这样做的规范方法是为结果对象{ errors: Error[], values: string[] }定义一个 monoid 实例,然后使用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))
  );
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM