Skip to content
Giulio Canti edited this page Sep 2, 2022 · 2 revisions

How-to

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
)
Clone this wiki locally