So last time we looked at Maybe
, which can be
-
Just<thing>
-
or
-
Nothing
-
compositionality
-
(hopefully) easier to follow
-
generally feeling clever
- Let's look back to our horse getting
const getHorse = (name: string): Maybe<Horse> => {
const found = goodHorses.find(horse => horse.name === name)
return found ? just(found) : nothing()
}
nothing
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 maybeHorse
try {
maybeHorse = 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 };
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...
- You are very smart.
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
Maybe
: -
alt :: Maybe A -> Maybe A -> Maybe A
-
Let's try fixing our horse issues with these: