Skip to content

jacob-alford/bifold-traverse

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

40 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation




bifold-traverse

Bifoldable and Bitraversable typeclasses for fp-ts 2.x

NPM Version Coverage Status Vulnerabilities



Welcome

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.

Installation

Uses fp-ts as a peer dependency. Read more about peer dependencies at nodejs.org.

Yarn

yarn add @jacob-alford/bifold-traverse

NPM

npm install @jacob-alford/bifold-traverse

PNPM

pnpm add @jacob-alford/bifold-traverse

Documentation

Explanation

Bifoldable

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.

Bitraversable

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.