Alternative Paths
Option Type #
When dealing with functions, we need to harden our function to make it total function. Meaning that it is defined for all possible inputs. Otherwise, our function is partial and it may return null which could result to break the chain of function executions as null refers to nothing so we no longer need to pass it over the other function.
And here comes the Option type that wraps the output of a function and indicate whether it has a value or not. Its more like a unified way to make our function robust.
Option is a type that return a Some that has a value or a None that has nothing. In our function logic, we wrap the calculation output inside Some object. And if there is no value, then we return None object. These two types are a subset of Option. But why we need more two types than just an Option? We need Some and None to differentiate the null structurally other than just making a new type MaybeNumber from number and null and wrap it inside option, then how do we recognize the null? we need to do exhaustive checking every time you implement a function.
An example of that, consider a function A that take an input, perform its intended calculation and return an output. And a function B same as function A takes input and returns an output. Now we compose these two functions. And in function A there is one of its input that returns a null, now the null is passed to the other function, what do you expect? Yes, unexpected behavior, so it NEEDS to consider null on its input mapping.
Example of using Options
// Define types
type Option<T> = Some<T> | None
type Some<T> = {
readonly kind: 'Some'
readonly value: T
}
type None = {
readonly kind: 'None'
}
// Helper functions for creating those types
const some = <T>(value: T): Option<T> => ({
kind: 'Some',
value
})
const none: Option<never> = {
kind: 'None'
}
// Type guard
const isNone = <T>(opt: Option<T>): opt is None => opt.kind === 'None'
We use the types to wrap the value within the two functions
type SquareRoot = (x: Option<number>) => Option<number>
const sqrt: SquareRoot = x => isNone(x) ?
none :
x.value < 0 ?
none :
some(Math.sqrt(x.value))
type DivideBy = (x: number) => Option<number>
const divideBy: DivideBy = x => x === 0 ? none : some(2 / x)
const composed = compose(sqrt, divideBy)
We must do checking using the created type guard to ensure the value exists, otherwise we return none. This can be easily fixed using Functors.
console.log(composed(0))
console.log(composed(-2))
console.log(composed(2))
/*
{ kind: 'None' }
{ kind: 'None' }
{ kind: 'Some', value: 1 }
*/
Now our function is safely a total function.
Either #
Similar to Option where it differentiates whether a value exists or absent, but it extends to give a feedback why the value is absence. Its useful to propagates error messages.
type Either<L, R> = Left<L> | Right<R>
Left is a type that holds a feedback when an operation fails, while Right for successful operation.
type Left<L> = {
readonly kind: 'Left'
readonly value: L
}
type Right<R> = {
readonly kind: 'Right'
readonly value: R
}
Now we create a utility functions for creating those types
const left = <L, R = never>(value: L): Either<L, R> => ({
kind: 'Left',
value
})
const right = <R, L = never>(value: R): Either<L, R> => ({
kind: 'Right',
value
})
Using Either type
type Increment = (x: number) => number
const increment = x => x + 1
type DivideBy = (x: number) => Either<string, number>
const divideBy: DivideBy = x => x === 0 ? left('Cannot divide by zero') : right(2 / x)
// Type guard
const isLeft = <L, R>(x: Either<L, R>): x is Left<L> => x.kind === 'Left'
// Composing two functions
const composed = compose(
(x: Either<string, number>) => isLeft(x) ? x : right(increment(x.value)),
divideBy
)
console.log(composed(-1))
console.log(composed(0))
console.log(composed(2))
console.log(composed(4))
/*
{ kind: 'Right', value: -1 }
{ kind: 'Left', value: 'Cannot divide by zero' }
{ kind: 'Right', value: 2 }
{ kind: 'Right', value: 1.5 }
*/