[英]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
);
}
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和继续Task
或TaskEither
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.需要注意的一件事是,这里我们在元素数组上应用非纯函数(包装版本中的
.attr
或attrIO
)。 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.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.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 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.