Skip to content

Latest commit

 

History

History
290 lines (207 loc) · 5.6 KB

lesson2-either-tuple.md

File metadata and controls

290 lines (207 loc) · 5.6 KB

Part 2

If it isn't there, why isn't it?

So last time we looked at Maybe, which can be

  • Just<thing>

  • or

  • Nothing

We've certainly gained some things

  • compositionality

  • (hopefully) easier to follow

  • generally feeling clever

But what have we lost?

  • 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.

Errors

  • 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?

Enter, Either

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

A note

  • You can also see this called Result with Failure and Success

  • Either and Result 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.

Let's crack open a couple of constructor functions

  • 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" }

An example, if we must

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" }

A recap regarding Horses

  • 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
  }
];

Step 1 - Find Horse

  • 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`)
}

Step 2 - Tidy Horse Name

  • tidyHorseName :: Horse -> Horse
const tidyHorseName = (horse: Horse): Horse => 
  ({
    ...horse,
    name: horse.name.charAt(0).toUpperCase() + 
        horse.name.slice(1).toLowerCase()
  })

Step 3 - Standardise Horse

  • 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"
  })
};

What we want

  • horseFinder2 :: String -> Either String StandardHorse

  • Over to you...

Great work.

  • You are very smart.

Alternatives - a nice pattern

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.

Great

  • 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: