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 a
s) 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.
Yes, I want to distinguish between a single success and failure case at the end
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 ]
// }