Bifoldable and Bitraversable typeclasses for fp-ts 2.x
bifold-traverse
is a port of purescript-foldable-traversable's Bifoldable
and Bitraversable
typeclasses to fp-ts 2.x.
Bifoldable and Bitraversable are typeclasses with instances in Either, IOEither, ReadonlyTuple, and Separated. Additional utilities bisequence
and bitraverse
are also provided for TaskEither.
Uses fp-ts
as a peer dependency. Read more about peer dependencies at nodejs.org.
yarn add @jacob-alford/bifold-traverse
npm install @jacob-alford/bifold-traverse
pnpm add @jacob-alford/bifold-traverse
fp-ts
comes with a built in Foldable
typeclass which is built for data-types that have a single type parameter. For data-types with two type-parameters this can be limiting as certain assumptions are made about how to handle the first type parameter.
Bifoldable
is useful for when you want to collapse a data-type with two type parameters into a single type while also considering the first type parameter. For example, here is an example using bifoldMap
from the exported These
module:
import { bifoldMap } from '@jacob-alford/bifold-traverse/These'
import * as Str from 'fp-ts/string'
import * as Th from 'fp-ts/These'
const uppercaseFold: (fa: Th.These<string, string>) => string = bifoldMap(Str.Monoid)(
Str.toLowercase,
Str.toUppercase,
)
assert.deepStrictEqual(uppercaseFold(Th.left('Foo')), 'foo')
assert.deepStrictEqual(uppercaseFold(Th.right('bar')), 'BAR')
assert.deepStrictEqual(uppercaseFold(Th.both('foo', 'bar')), 'fooBAR')
Here we fold over These
and collapse the sum type into a common monoid while mapping each type parameter.
Like Foldable
, Traversable
only accounts for a single type parameter. And like Bifoldable
, Bitraversable
allows you to account for both type-parameters while traversing and sequencing.
The following example illustrates the limitation of using traverse
on two-parameter data-types. We are able to narrow Either's success parameter to a number, but we are unable to narrow the error parameter.
import * as E from 'fp-ts/Either'
import * as O from 'fp-ts/Option'
import { pipe } from 'fp-ts/function'
const e: E.Either<string | Error, string | number> = E.left('string')
const result = pipe(
e,
E.traverse(O.Applicative)(O.fromPredicate((s): s is number => typeof s === 'number')),
)
// result: Option<Either<string | Error, number>>
Unlike Traversable
, Bitraversable
is built for data-types with two type parameters, and allows us to traverse both type parameters at the same time.
The following admittedly complex example traverses a Separated
data type with a collection of failures and successes, filtering both data types and short-circuiting on a critical error.
import { bitraverse } from '@jacob-alford/bifold-traverse/Separated'
import * as E from 'fp-ts/Either'
import * as O from 'fp-ts/Option'
import * as RA from 'fp-ts/ReadonlyArray'
import * as Sep from 'fp-ts/Separated'
import { flow } from 'fp-ts/function'
type RelevantError = { readonly _tag: 'relevant-error'; message: string }
type IrrelevantError = { readonly _tag: 'irrelevant-error' }
type CriticalError = { readonly _tag: 'critical-error'; reason: string }
type Error = RelevantError | IrrelevantError | CriticalError
const relevantError: (message: string) => Error = message => ({
_tag: 'relevant-error',
message,
})
const irrelevantError: Error = { _tag: 'irrelevant-error' }
const criticalError: (reason: string) => Error = reason => ({
_tag: 'critical-error',
reason,
})
const parseSemicolonList: (s: string) => O.Option<ReadonlyArray<string>> = flow(
O.fromPredicate(s => /^((.*);)(.*)(;?)$/.test(s)),
O.map(s => s.split(';')),
)
const collectErrorsAndParseSuccesses: (
fa: Sep.Separated<ReadonlyArray<Error>, ReadonlyArray<string>>,
) => E.Either<
CriticalError,
Sep.Separated<ReadonlyArray<RelevantError>, ReadonlyArray<string>>
> = bitraverse(E.Applicative)(
flow(
E.traverseArray(
E.fromPredicate(
(err): err is RelevantError | IrrelevantError => err._tag !== 'critical-error',
err => (err._tag === 'critical-error' ? `Error: ${err.reason}` : `Error`),
),
),
E.bimap(
err => ({ _tag: 'critical-error', reason: err }),
RA.filter((err): err is RelevantError => err._tag === 'relevant-error'),
),
),
flow(
O.traverseArray(parseSemicolonList),
O.map(RA.flatten),
E.fromOption(() => ({
_tag: 'critical-error',
reason: 'failed to parse string',
})),
),
)
assert.deepStrictEqual(
collectErrorsAndParseSuccesses(
Sep.separated(
[relevantError('first error'), relevantError('second error'), irrelevantError],
['foo;bar;baz', 'qux;quux;quuz'],
),
),
E.right(
Sep.separated(
[relevantError('first error'), relevantError('second error')],
['foo', 'bar', 'baz', 'qux', 'quux', 'quuz'],
),
),
)
assert.deepStrictEqual(
collectErrorsAndParseSuccesses(
Sep.separated(
[
relevantError('first error'),
relevantError('second error'),
criticalError('critical error'),
],
['foo;bar;baz', 'qux;quux;quuz'],
),
),
E.left(criticalError('Error: critical error')),
)
bisequence
here is useful for the same reasons as bitraverse
but for when the inner HKT is already established.