-
-
Notifications
You must be signed in to change notification settings - Fork 502
Either
Giulio Canti edited this page Sep 2, 2022
·
2 revisions
Problem
I want to map a string array to an integer array and compute the total sum. Mapping a string to an int implies that the string might be a number, but it might not as well. So, question is, how do I structure the program so I can handle the resulting exception when a string is actually not a number? How to handle exceptions properly in a functional style?
Solution
import * as assert from 'assert'
import { pipe } from 'fp-ts/function'
import * as M from 'fp-ts/Monoid'
import * as N from 'fp-ts/number'
import * as E from 'fp-ts/Either'
import * as RA from 'fp-ts/ReadonlyArray'
const parseInteger = (s: string): E.Either<string, number> => {
const n = parseInt(s, 10)
return isNaN(n) ? E.left(`${s} is not a number`) : E.right(n)
}
const getSum: (numbers: ReadonlyArray<number>) => number = M.concatAll(
N.MonoidSum
)
const solution = (input: ReadonlyArray<string>): E.Either<string, number> => {
// const parsing: readonly E.Either<string, number>[]
const parsing = pipe(input, RA.map(parseInteger))
// const numbers: Either<string, readonly number[]>
const numbers = pipe(parsing, RA.sequence(E.Applicative))
// const sum: E.Either<string, number>
const sum = pipe(numbers, E.map(getSum))
return sum
}
assert.deepStrictEqual(solution(['1', '2', '3']), E.right(6))
assert.deepStrictEqual(solution(['1', 'a', '3']), E.left('a is not a number'))
You can rewrite the solution to a single pipeline
const solution = (input: ReadonlyArray<string>): E.Either<string, number> => {
return pipe(
input,
RA.map(parseInteger),
RA.sequence(E.Applicative),
E.map(getSum)
)
}
Note that map + sequence = traverse
, you can refactor the pipeline to
const solution = (input: ReadonlyArray<string>): E.Either<string, number> => {
return pipe(input, RA.traverse(E.Applicative)(parseInteger), E.map(getSum))
}
Note that you get only the first error
assert.deepStrictEqual(solution(['1', 'a', 'b']), E.left('a is not a number'))
If you want to get all errors
const solution = (
input: ReadonlyArray<string>
): E.Either<ReadonlyArray<string>, number> => {
return pipe(
input,
RA.traverse(E.getApplicativeValidation(RA.getSemigroup<string>()))((s) =>
pipe(s, parseInteger, E.mapLeft(RA.of))
),
E.map(getSum)
)
}
assert.deepStrictEqual(solution(['1', '2', '3']), E.right(6))
assert.deepStrictEqual(solution(['1', 'a', '3']), E.left(['a is not a number']))
assert.deepStrictEqual(
solution(['1', 'a', 'b']),
E.left(['a is not a number', 'b is not a number'])
)
Alternatively if you just want to skip bad inputs but keep adding good ones
const solution = (input: ReadonlyArray<string>): number => {
return pipe(input, RA.filterMap(parseInteger), getSum)
}
assert.deepStrictEqual(solution(['1', '2', '3']), 6)
assert.deepStrictEqual(solution(['1', 'a', '3']), 4)
Optionally since input
is repeated you can get rid of pipe
and use flow
import { flow } from 'fp-ts/function'
const solution: (input: ReadonlyArray<string>) => number = flow(
RA.filterMap(parseInteger),
getSum
)