有人可以帮我在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
,然后继续{ {1}}或Task
TypeScript错误为TaskEither
Type 'Either<Error, string[]>' is not assignable to type 'IO<unknown>'
答案 0 :(得分:6)
如果要“正确”执行操作,则需要将所有非确定性(非纯)函数调用包装在IO或IOEither中(取决于它们是否可以失败)。
因此,首先让我们定义哪些函数调用是“纯”的,哪些不是。我发现最容易想到的是-如果函数ALWAYS为相同的输入提供相同的输出,并且不会导致任何可观察的副作用,那是纯粹的。
“相同输出”并不意味着引用相等,而是结构/行为相等。因此,如果您的函数返回了另一个函数,则此返回的函数可能不是同一函数对象,但是必须具有相同的行为(对于原始函数而言,它应该是纯函数)。
因此,按照这些术语,是正确的:
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
会返回与原始代码中tests
变量中相同元素的IO。
免责声明:我尚未通过编译器运行代码,它可能会有一些错别字或错误。您可能还需要付出一些努力,以使其全部成为强类型。
编辑:
如果您想保留有关错误的信息(在这种情况下,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。如果有错误,则此操作将汇总错误,如果没有错误,则将汇总值。如果有任何错误,此解决方案将丢失有关正确值的信息。 与上面的方法相比,这种方法在实践中更为罕见,并且实施起来更棘手。一种常见的用例是验证检查,为此,有一个monad转换器https://gcanti.github.io/fp-ts/modules/ValidationT.ts.html。我没有太多经验,所以不能在这个话题上说更多。
IO<{ errors: Error[], values: string[] }>
-返回包含错误和值的对象的IO。此解决方案也不会丢失任何信息,但实施起来会有些棘手。规范的做法是为结果对象{ 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))
);
}