Skip to content
Giulio Canti edited this page Sep 2, 2022 · 12 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 O from 'fp-ts/Option'
import * as RA from 'fp-ts/ReadonlyArray'

const parseInteger = (s: string): O.Option<number> => {
  const n = parseInt(s, 10)
  return isNaN(n) ? O.none : O.some(n)
}

const getSum: (numbers: ReadonlyArray<number>) => number = M.concatAll(
  N.MonoidSum
)

const solution = (input: ReadonlyArray<string>): O.Option<number> => {
  // const parsing: readonly O.Option<number>[]
  const parsing = pipe(input, RA.map(parseInteger))
  // const numbers: O.Option<readonly number[]>
  const numbers = pipe(parsing, RA.sequence(O.Applicative))
  // const sum: O.Option<number>
  const sum = pipe(numbers, O.map(getSum))
  return sum
}

assert.deepStrictEqual(solution(['1', '2', '3']), O.some(6))
assert.deepStrictEqual(solution(['1', 'a', '3']), O.none)

You can rewrite the solution to a single pipeline

const solution = (input: ReadonlyArray<string>): O.Option<number> => {
  return pipe(
    input,
    RA.map(parseInteger),
    RA.sequence(O.Applicative),
    O.map(getSum)
  )
}

Note that map + sequence = traverse, you can refactor the pipeline to

const solution = (input: ReadonlyArray<string>): O.Option<number> => {
  return pipe(input, RA.traverse(O.Applicative)(parseInteger), O.map(getSum))
}

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
)

Footguns

APIs returning a sentinel value

import { pipe } from 'fp-ts/function'

const doSomethingWithIndex = (n: number): number => n * 2

pipe(
  ['a', 'b', 'c'].findIndex((s) => s.length > 2), // no error
  doSomethingWithIndex,
  console.log
) // => -2, because findIndex returns -1 :facepalm: the type checker can't help us here

import * as RA from 'fp-ts/ReadonlyArray'

pipe(
  ['a', 'b', 'c'],
  RA.findIndex((s) => s.length > 2), // error
  doSomethingWithIndex,
  console.log
)

APIs returning undefined

import { pipe } from '../src/function'

pipe(
  ['a', undefined, 'c'].find((s) => s === undefined || s === 'c'),
  console.log
) // undefined (<= what does it means? Did it found an element or not?)

import * as RA from 'fp-ts/ReadonlyArray'

pipe(
  ['a', undefined, 'c'],
  RA.findFirst((s) => s === undefined || s === 'c'),
  console.log
) // some(undefined) (<= found an element which is `undefined`)
Clone this wiki locally