Skip to content

Instantly share code, notes, and snippets.

@pete-murphy
Last active April 30, 2024 13:50
Show Gist options
  • Save pete-murphy/22c54d3fa713c69d259171a69bf8186d to your computer and use it in GitHub Desktop.
Save pete-murphy/22c54d3fa713c69d259171a69bf8186d to your computer and use it in GitHub Desktop.
Error collecting

Often we want to iterate through a collection of items, performing some effect for each item. This means we want some function that looks like

(a -> f b) -> t a -> result

where a -> f b is our effectful computation, t a is our collection (of as) and result could take a few different shapes depending on the requirements of our program, especially in the common case when the effect f encapsulates some notion of failure (like TaskEither in fp-ts, or anything with ExceptT in its stack in Haskell).

Tip

The tl;dr is that you almost always want (some version of) traverse in this situation—so often that "the answer is always traverse" has become a meme.

Are you going to branch on the result of the computation?

Yes, I want to distinguish between a single success and failure case at the end

Is there a notion of partial success (succeed with a warning)?

Yes

Use traverse with These.

If everything at least partially succeeds, collect all successes and all warnings. Otherwise report all failures and all warnings.

import { task as T, taskThese as TTh, readonlyArray as RA } from "fp-ts"
import { flow, pipe } from "fp-ts/function"

const f = (str: string) => {
  const n = parseInt(str)
  if (isNaN(n)) {
    return TTh.left(`${str} is not a number`)
  }
  if (n < 4) {
    return TTh.right(n)
  }
  return TTh.both(`${n} is not less than 4`, n)
}

const allSuccesses = pipe(
  ["1", "2", "3"],
  RA.traverse(TTh.getApplicative(T.ApplyPar, RA.getSemigroup<string>()))(
    flow(f, TTh.mapLeft(RA.of))
  )
)

const someWarnings = pipe(
  ["1", "2", "3", "4", "5"],
  RA.traverse(TTh.getApplicative(T.ApplyPar, RA.getSemigroup<string>()))(
    flow(f, TTh.mapLeft(RA.of))
  )
)

const someFailures = pipe(
  ["1", "2", "c", "4", "e"],
  RA.traverse(TTh.getApplicative(T.ApplyPar, RA.getSemigroup<string>()))(
    flow(f, TTh.mapLeft(RA.of))
  )
)

allSuccesses().then(console.log)
// { _tag: 'Right', right: [ 1, 2, 3 ] }

someWarnings().then(console.log)
// {
//   _tag: 'Both',
//   left: [ '4 is not less than 4', '5 is not less than 4' ],
//   right: [ 1, 2, 3, 4, 5 ]
// }

someFailures().then(console.log)
// {
//   _tag: 'Left',
//   left: [ 'c is not a number', '4 is not less than 4', 'e is not a number' ]
// }
No
If everything succeeds I want to collect all successes.
Otherwise report first failure.

Use traverse with default Either Applicative instance.

import { taskEither as TE, readonlyArray as RA } from "fp-ts"
import { pipe } from "fp-ts/function"

const f = (str: string) => {
  const n = parseInt(str)
  if (isNaN(n)) {
    return TE.left(`${str} is not a number`)
  }
  return TE.right(n)
}

const allSuccesses = pipe(
  ["1", "2", "3", "4", "5"],
  RA.traverse(TE.ApplicativePar)(f)
)

const someFailures = pipe(
  ["1", "2", "c", "4", "e"],
  RA.traverse(TE.ApplicativePar)(f)
)

allSuccesses().then(console.log)
// { _tag: 'Right', right: [ 1, 2, 3, 4, 5 ] }

someFailures().then(console.log)
// { _tag: 'Left', left: 'c is not a number' }
Otherwise report all failures.

Use traverse with validation Applicative instance.

import { taskEither as TE, task as T, readonlyArray as RA } from "fp-ts"
import { flow, pipe } from "fp-ts/function"

const f = (str: string): TE.TaskEither<string, number> => {
  const n = parseInt(str)
  if (isNaN(n)) {
    return TE.left(`${str} is not a number`)
  }
  return TE.right(n)
}

const allSuccesses = pipe(
  ["1", "2", "3", "4", "5"],
  RA.traverse(
    TE.getApplicativeTaskValidation(T.ApplicativePar, RA.getSemigroup<string>())
  )(flow(f, TE.mapError(RA.of)))
)

const someFailures = pipe(
  ["1", "2", "c", "4", "e"],
  RA.traverse(
    TE.getApplicativeTaskValidation(T.ApplicativePar, RA.getSemigroup<string>())
  )(flow(f, TE.mapError(RA.of)))
)

allSuccesses().then(console.log)
// { _tag: 'Right', right: [ 1, 2, 3, 4, 5 ] }

someFailures().then(console.log)
// { _tag: 'Left', left: [ 'c is not a number', 'e is not a number' ] }
If anything succeeds I want to collect first success.

Both these examples are a bit awkward in fp-ts because it requires a startWith argument. In Haskell, there's a Foldable1 class for non-empty foldable containers, which could use asum1 to avoid needing a "starting" value.

Otherwise report last failure.

Use altAll

import { alt as Alt, taskEither as TE, readonlyArray as RA } from "fp-ts"
import { pipe } from "fp-ts/function"

const f = (str: string): TE.TaskEither<string, number> => {
  const n = parseInt(str)
  if (isNaN(n)) {
    return TE.left(`${str} is not a number`)
  }
  return TE.right(n)
}

const allSuccesses = pipe(
  ["2", "3", "4", "5"],
  RA.map(f),
  Alt.altAll(TE.Alt)(f("1"))
)

const startWithFailure = pipe(
  ["2", "c", "4", "e"],
  RA.map(f),
  Alt.altAll(TE.Alt)(f("a"))
)

const allFailures = pipe(["b", "c"], RA.map(f), Alt.altAll(TE.Alt)(f("a")))

allSuccesses().then(console.log)
// { _tag: 'Right', right: 1 }

startWithFailure().then(console.log)
// { _tag: 'Right', right: 2 }

allFailures().then(console.log)
// { _tag: 'Left', left: 'c is not a number' }
Otherwise report all failures.

Use altAll with validation Alt instance.

import { alt as Alt, taskEither as TE, readonlyArray as RA } from "fp-ts"
import { flow, pipe } from "fp-ts/function"

const f = (str: string): TE.TaskEither<string, number> => {
  const n = parseInt(str)
  if (isNaN(n)) {
    return TE.left(`${str} is not a number`)
  }
  return TE.right(n)
}

const g = flow(f, TE.mapError(RA.of))

const allSuccesses = pipe(
  ["2", "3", "4", "5"],
  RA.map(g),
  Alt.altAll(TE.getAltTaskValidation(RA.getSemigroup<string>()))(g("1"))
)

const startWithFailure = pipe(
  ["2", "c", "4", "e"],
  RA.map(g),
  Alt.altAll(TE.getAltTaskValidation(RA.getSemigroup<string>()))(g("a"))
)

const allFailures = pipe(
  ["b", "c"],
  RA.map(g),
  Alt.altAll(TE.getAltTaskValidation(RA.getSemigroup<string>()))(g("a"))
)

allSuccesses().then(console.log)
// { _tag: 'Right', right: 1 }

startWithFailure().then(console.log)
// { _tag: 'Right', right: 2 }

allFailures().then(console.log)
// {
//   _tag: 'Left',
//   left: [ 'a is not a number', 'b is not a number', 'c is not a number' ]
// }

No, I just want to run the computation and collect all successes and all failures

Use wilt

import { taskEither as TE, task as T, readonlyArray as RA } from "fp-ts"
import { pipe } from "fp-ts/function"

const f = (str: string) => {
  const n = parseInt(str)
  if (isNaN(n)) {
    return TE.left(`${str} is not a number`)
  }
  return TE.right(n)
}

const allSuccesses = pipe(
  ["1", "2", "3", "4", "5"],
  RA.wilt(T.ApplicativePar)(f)
)

const someFailures = pipe(
  ["1", "2", "c", "4", "e"],
  RA.wilt(T.ApplicativePar)(f)
)

allSuccesses().then(console.log)
// { left: [], right: [ 1, 2, 3, 4, 5 ] }

someFailures().then(console.log)
// {
//   left: [ 'c is not a number', 'e is not a number' ],
//   right: [ 1, 2, 4 ]
// }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment