将 IO 映射到 fp-ts 中的 Either 数组
Map IO to array of Either 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);
我可以使用 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
并继续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)))),
);
如果你想这样做"properly",那么你需要将所有非确定性(非纯)函数调用包装在 IO 或 IOEither 中(取决于它们是否可以失败)。
所以首先让我们定义哪些函数调用是 "pure" 哪些不是。我发现最容易想到的是这样 - 如果函数总是为 相同的输入 提供 相同的输出 并且不会导致任何 可观察到副作用,那就纯了。
"Same output"不是指引用相等,而是structural/behaviour相等。因此,如果您的函数 return 是另一个函数,这个 returned 函数可能不是同一个函数对象,但它的行为必须相同(对于被认为是纯函数的原始函数)。
所以在这些术语中,以下是正确的:
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'));
然后 assemble 一切都在一起:
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
return 一个 IO<string[]>
。要在那里放置错误信息,您可以这样做:
IO<Either<Error, string>[]>
- 一个 IO return 是一个数组,其中每个元素都是错误或值。要使用它,您仍然需要稍后进行过滤以消除错误。这是最灵活的解决方案,因为您不会丢失任何信息,但感觉也有点无用,因为 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[]>
- return 要么是错误要么是值数组的 IO。这里最常见的做法是 return 当你得到第一个缺失的属性时出错,如果所有值都没有错误, return 一个值数组。因此,如果有任何错误,此解决方案将再次丢失有关正确值的信息,并且会丢失有关除第一个错误之外的所有错误的信息。
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[]>
- return 是错误数组或值数组的 IO。如果有任何错误,此方法将聚合错误,如果没有错误,则聚合值。如果有任何错误,此解决方案将丢失有关正确值的信息。
这种方法在实践中比上述方法更罕见,实施起来也更棘手。一个常见的用例是验证检查,为此有一个 monad 转换器 https://gcanti.github.io/fp-ts/modules/ValidationT.ts.html。我没有太多经验,所以不能就这个话题多说。
IO<{ errors: Error[], values: string[] }>
- return 是包含错误和值的对象的 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))
);
}
谁能帮我弄清楚如何在 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
并继续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)))),
);
如果你想这样做"properly",那么你需要将所有非确定性(非纯)函数调用包装在 IO 或 IOEither 中(取决于它们是否可以失败)。
所以首先让我们定义哪些函数调用是 "pure" 哪些不是。我发现最容易想到的是这样 - 如果函数总是为 相同的输入 提供 相同的输出 并且不会导致任何 可观察到副作用,那就纯了。
"Same output"不是指引用相等,而是structural/behaviour相等。因此,如果您的函数 return 是另一个函数,这个 returned 函数可能不是同一个函数对象,但它的行为必须相同(对于被认为是纯函数的原始函数)。
所以在这些术语中,以下是正确的:
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'));
然后 assemble 一切都在一起:
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
return 一个 IO<string[]>
。要在那里放置错误信息,您可以这样做:
IO<Either<Error, string>[]>
- 一个 IO return 是一个数组,其中每个元素都是错误或值。要使用它,您仍然需要稍后进行过滤以消除错误。这是最灵活的解决方案,因为您不会丢失任何信息,但感觉也有点无用,因为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[]>
- return 要么是错误要么是值数组的 IO。这里最常见的做法是 return 当你得到第一个缺失的属性时出错,如果所有值都没有错误, return 一个值数组。因此,如果有任何错误,此解决方案将再次丢失有关正确值的信息,并且会丢失有关除第一个错误之外的所有错误的信息。
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[]>
- return 是错误数组或值数组的 IO。如果有任何错误,此方法将聚合错误,如果没有错误,则聚合值。如果有任何错误,此解决方案将丢失有关正确值的信息。
这种方法在实践中比上述方法更罕见,实施起来也更棘手。一个常见的用例是验证检查,为此有一个 monad 转换器 https://gcanti.github.io/fp-ts/modules/ValidationT.ts.html。我没有太多经验,所以不能就这个话题多说。
IO<{ errors: Error[], values: string[] }>
- return 是包含错误和值的对象的 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))
);
}