Если вы хотите сделать это «должным образом», то вам нужно обернуть все недетерминированные c (не чистые) вызовы функций в IO или IOEither (в зависимости от того, могут они или не могут выйти из строя).
Итак, сначала давайте определим, какие вызовы функций являются «чистыми», а какие нет. Самое простое, о чем я могу подумать, это так: если функция ВСЕГДА выдает одинаковый вывод для того же входа и не вызывает наблюдаемого side- эффекты, то это чисто.
«Тот же вывод» не означает ссылочное равенство, это означает структурное / поведенческое равенство. Поэтому, если ваша функция возвращает другую функцию, эта возвращаемая функция может быть не тем же функциональным объектом, но она должна вести себя одинаково (чтобы исходная функция считалась чистой).
Итак, в этих терминах следующее true:
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
возвращает вам IO тех же элементов, которые были в вашей переменной tests
в исходном коде.
Отказ от ответственности: я не запускал код через компилятор, возможно, он содержит некоторые опечатки или ошибки. Возможно, вам также понадобится приложить некоторые усилия, чтобы все это было строго напечатано.
EDIT :
Если вы хотите сохранить информацию об ошибках (в данном случае, пропущенных 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, который возвращает либо массив ошибок ИЛИ массив значений. Это объединяет ошибки, если они есть, и объединяет значения, если ошибок нет. Это решение теряет информацию о правильных значениях, если есть какие-либо ошибки.
Этот подход на практике встречается реже, чем описанные выше, и его сложнее реализовать. Одним из распространенных вариантов использования является проверка достоверности, для которой есть монадный трансформатор https://gcanti.github.io/fp-ts/modules/ValidationT.ts.html. У меня нет особого опыта в этом, поэтому я не могу сказать больше об этом topi c.
IO<{ errors: Error[], values: string[] }>
- 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))
);
}