-
-
Notifications
You must be signed in to change notification settings - Fork 502
Option
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
)
Problem
I have two
Option<T[]>
and want to operate on the concatenated value of both, i.e. forO.some([1, 2])
andO.some([3])
I'd like to work on[1, 2, 3]
while in the case any of the two isO.none
I still want the value of the other. What's the most convenient way to concatenate those?
Solution
import * as assert from 'assert'
import * as O from 'fp-ts/Option'
import * as RA from 'fp-ts/ReadonlyArray'
const solution = <A>(
o1: O.Option<ReadonlyArray<A>>,
o2: O.Option<ReadonlyArray<A>>
): O.Option<ReadonlyArray<A>> => {
return O.getMonoid(RA.getSemigroup<A>()).concat(o1, o2)
}
assert.deepStrictEqual(solution(O.some([1, 2]), O.some([3])), O.some([1, 2, 3]))
assert.deepStrictEqual(solution(O.none, O.some([3])), O.some([3]))
assert.deepStrictEqual(solution(O.some([1, 2]), O.none), O.some([1, 2]))
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`)
Both data types could be used to model partial functions making them total which is a necessary requirement for functional programming. A partial function is not a function in the mathematical sense (the primary goal of functional programming is to model computation via mathematical functions). Both data types can thus be used to model computations that may "fail" in some sense e.g. accessing an out-of-bound index, reading a file from the file system, dividing a number by 0.
We can reasonably draw similarities between the Some<A>
and Right<A>
data types, they are the same very same data type with a different name.
In fact, both types are also equivalent to A
itself and do not need the additional ceremony of being "wrapped" in a parametric type at all! Neither Right<A>
nor Some<A>
add any information over A
alone.
Thus Right<A>
and Some<A>
only make sense in the context of being members of the sum types they form along Left<E>
in the first case and None
in the second case.
Thus, the difference between the Either<E, A>
and Option<A>
data types is the fact that Left<E>
and None
, differently from Some<A>
and Right<A>
are not equivalent at all.
-
None
is a unit type. Since it has always the same value: it does not hold any kind of information. -
Left<E>
is a parametric type. It can hold any kind of information of the generic typeE
.
Suppose we want to model the function head
which given an Array<A>
data type will give us the first element of type A
.
declare const head: <A>(as: A[]) => A
This function with that signature is impossible to implement (try if you want as an exercise!). It is a partial function.
The value of Array<A>
might be []
, thus head cannot return a value.
Let's make the function total leveraging the Either<E,A>
or Option<A>
data types:
import { none, some, Option } from "fp-ts/Option"
const head: <A>(as: A[]) => Option<A> = as => as.length === 0 ? none : some(as[0])
As we can see it is quite trivial to implement a total version of head
using the Option data type.
Let's use Either<E, A>
.
First of all, we need to find how we want to model the failure, as in the case of Left<E>
we do need to provide a value for E
. Let's settle for a very expressive Left<"Array is empty">
.
import { left, right, Either } from "fp-ts/Option"
const head: <A>(as: A[]) => Either<"Array is empty", A> = as => as.length === 0 ? left("Array is empty") : right(as[0])
There are two major problems with the head
function above:
-
the first regards consumption and implementation of the function: it is not very handy to implement an api for which we have to come up with a value for
E
inLeft<E>
. -
The second reason is conceptual:
Left<"Array is empty">
is a unit type, it does not contain any additional information thatNone
does not! That is:Either<"Array is empty", A>
andOption<A>
are the very same exact type for all practical and conceptual purposes, with the first one just going through more ceremonies.
But what if I have various computations that may "fail", wouldn't I want to know the reason, the fact that the array is empty might be one of them?
This is a very valid concern that is very common in practice.
Imagine the following scenario, we want to take the first character of the first string an Array<string>
: this operation may fail because either the array or the first string is empty.
This could have a very real use case: the array of strings may represent different sentences and we're tasked to capitalize the first letter of every sentence.
import { Option } from "fp-ts/Option"
import { flow } from "fp-ts/Function"
// takes the first element of an Array<A>
declare const head: <A>(as: A[]) => Option<A>
// takes the first character of a string, which again, might be empty
declare const getFirstCharacter: (s: string) => Option<string>
const firstCharacterOfTheFirstString: (ss: string[]) => Option<string> = flow(head, O.flatMap(getFirstCharacter))
As we can see, there is no way to tell whether the array of strings was empty, or the string was empty in case None
is returned by firstCharacterOfTheFirstString
!
If we want this information, an Either
type is necessary.
Let's change the signature of firstCharacterOfTheFirstString
:
declare const: firstCharacterOfTheFirstString: (ss: string[]) => Either<"Array is empty" | "String is empty", string>
This may look like a "win" for Either
indeed: afterall providing that additional information in Left<E>
does provide some benefit.
It would be...if it wasn't for the fact that we can always transform an Option
in an Either
with a natural transformation (we can also do it the other way around).
Let's see a possible implementation:
import { fromOption, Either, chainW } from 'fp-ts/Either';
import { Option } from 'fp-ts/Option';
import { flow } from 'fp-ts/function';
// notice how both functions return an Option, not an Either
declare const head: <A>(as: A[]) => Option<A>;
declare const getFirstCharacter: (s: string) => Option<string>;
const firstCharacterOfTheFirstString: (ss: string[]) => Either<'Array is empty' | 'String is empty', string> = flow(
head,
fromOption(() => 'Array is empty' as const), // as const used to make TS infer the literal 'Array is empty' rather than 'string'
chainW(
flow(
getFirstCharacter,
fromOption(() => 'String is empty' as const)
)
)
);
As we can see going through natural transformations we can achieve all our goals:
- distinguish why the program has not returned us the first character of the first string
- preserve the simplest APIs for
head
andgetFirstCharacter
without using an awkward API (that contains the same information as Option for the reasons we have seen before)
Option:
-
there is one and only one reason for "failure", as in the case of
head
orgetFirstCharacter
. It is not possible to add any additional information because there isNone
(pun intended), there's one and only one reason those functions can fail (absence of the value). -
we are not interested in the reason it failed. It may make sense in our program that we only want to handle the "happy case" of indeed having
A
but we are not interested in handling the reason it failed.
Either:
- there are multiple reasons for "failure". Not the case of
head
orgetFirstCharacter
but definitely the case offirstCharacterOfTheFirstString
- we do want to handle the different reasons for failure in our program
Notice that fp-ts exposes a natural transformation from Either
to Option
as well. Even if you have a Left<E>
that contains multiple failure reasons you can always decide that you may not need reasons to handle that if it makes sense in your program. There are many reasons, e.g., why an http call, parsing a document or reading a file may fail, but your program may not need to handle those reasons.