So last time we looked at Option
, which can be
-
Some<thing>
-
or
-
None
-
compositionality
-
(hopefully) easier to follow
-
generally feeling clever
- Let's look back to our horse getting
const getHorse = (name: string): Option<Horse> => {
const found = goodHorses.find(horse => horse.name === name)
return found ? some(found) : none()
}
none
is all very well but it doesn't tell us why we are sitting here without any horse.
- One solutions could be to throw errors like the good old days?
const getHorse = (name: string): Horse => {
const found = goodHorses.find(horse => horse.name === name)
if (!found) {
throw Error(`Horse ${name} not found`)
}
return found
}
- ...and catch them down the line to see what happened.
let optionHorse
try {
optionHorse = getHorse('FAST-BOY')
} catch (e: string) {
// do something with the Error
}
- There's something wrong here though
That e
isn't really a string
, it's any
, as it could also be telling us we
are out of disk space or memory.
- What else can we do?
type Either<E, A> =
| { type: 'Left'; value: E }
| { type: 'Right'; value: A }
-
It represents any two outcomes, but usually...
-
Left
describes the failure case -
Right
describes the success case
-
You can also see this called
Result
withFailure
andSuccess
-
Either
andResult
are semantically the same -
If you wish to sound clever you can say they are
isomorphic
to one another. -
This means you can swap between the two at will without losing any information
-
We'll stick to
Either
though.
Left :: E -> Either<E, never>
const left = <E>(value: E): Either<E, never> => ({
type: 'Left',
value,
})
left('egg')
// { type: "Left", value: "egg" }
Right :: A -> Either<never, A>
const right = <A>(value: A): Either<never, A> => ({
type: 'Right',
value,
})
right('leg')
// { type: "Right", value: "leg" }
Now when something fails, we can say why
const divide = (
dividend: number,
divisor: number
): Either<string, number> => {
if (divisor === 0) {
return left('Cannot divide by zero')
}
return right(dividend / divisor)
}
- When things go well...
divide(10, 2)
// { type: "Right", value: 5 }
- Or when they don't...
divide(100, 0)
// { type: "Left", value: "Cannot divide by zero" }
- Let's go back to our beloved example involved horses, now with extra
Either
.
type Horse = {
type: 'HORSE'
name: string
legs: number
hasTail: boolean
}
- Here again, are our horses
const horses: Horse[] = [
{
type: 'HORSE',
name: 'CHAMPION',
legs: 3,
hasTail: false,
},
{
type: 'HORSE',
name: 'HOOVES_GALORE',
legs: 4,
hasTail: true,
},
]
getHorse :: String -> Either String Horse
const getHorse = (name: string): Either<string, Horse> => {
const found = horses.filter(horse => horse.name === name)
return found[0] ? right(found[0]) : left(`Horse ${name} not found`)
}
tidyHorseName :: Horse -> Horse
const tidyHorseName = (horse: Horse): Horse => ({
...horse,
name:
horse.name.charAt(0).toUpperCase() +
horse.name.slice(1).toLowerCase(),
})
- Some types...
type StandardHorse = {
name: string
hasTail: true
legs: 4
type: 'STANDARD_HORSE'
}
type TailCheckError =
| { type: 'HAS_NO_TAIL' }
| { type: 'TOO_MANY_LEGS' }
| { type: 'NOT_ENOUGH_LEGS' }
standardise :: Horse -> Either TailCheckError StandardHorse
const standardise = (
horse: Horse
): Either<TailCheckError, StandardHorse> => {
if (!horse.hasTail) {
return left({ type: 'HAS_NO_TAIL' })
}
if (horse.legs < 4) {
return left({ type: 'NOT_ENOUGH_LEGS' })
}
if (horse.legs > 4) {
return left({ type: 'TOO_MANY_LEGS' })
}
return right({
name: horse.name,
hasTail: true,
legs: 4,
type: 'STANDARD_HORSE',
})
}
-
horseFinder2 :: String -> Either String StandardHorse
-
Over to you...
Our code here suggests a fairly linear path, but the truth is that most things aren't so simple.
- Imagine for a moment, that there is a second source of horses.
const otherHorses: Horse[] = [
{
type: 'HORSE',
name: 'ROAST_BEEF',
legs: 2,
hasTail: false,
},
{
type: 'HORSE',
name: 'INFINITE_JEFF',
legs: 5,
hasTail: true,
},
]
-
Therefore, when doing
getHorse
we have two places we can look. -
The first place is preferable though.
- We could adapt this function to take the horse source as a parameter..
const getHorse2 = (possibleHorses: Horse[]) => (
name: string
): Either<string, Horse> => {
const found = possibleHorses.filter(horse => horse.name === name)
return found[0] ? right(found[0]) : left(`Horse ${name} not found`)
}
-
But how do we try one and then the other?
-
Now, what if, we had a function, with a type signature that looked like this?
-
alt :: Either E A -> Either E A -> Either E A
-
Or indeed, for
Option
: -
alt :: Option A -> Option A -> Option A
-
Let's try fixing our horse issues with these: