Reading this blog post – https://www.haskellforall.com/2021/05/the-trick-to-avoid-deeply-nested-error.html – I realised I don't understand why the 'trick' actually works in this situation:
{-# LANGUAGE NamedFieldPuns #-}
import Text.Read (readMaybe)
data Person = Person { age :: Int, alive :: Bool } deriving (Show)
example :: String -> String -> Either String Person
example ageString aliveString = do
age <- case readMaybe ageString of
Nothing -> Left "Invalid age string"
Just age -> pure age
if age < 0
then Left "Negative age"
else pure ()
alive <- case readMaybe aliveString of
Nothing -> Left "Invalid alive string"
Just alive -> pure alive
pure Person{ age, alive }
Specifically I'm struggling to understand why this bit
if age < 0
then Left "Negative age"
else pure ()
type checks.
Left "Negative age" has a type of Either String b
while
pure () is of type Either a ()
Why does this work the way it does?
EDIT: I simplified and re-wrote the code into bind operations instead of do block, and then saw Will's edit to his already excellent answer:
{-# LANGUAGE NamedFieldPuns #-}
import Text.Read (readMaybe)
newtype Person = Person { age :: Int} deriving (Show)
example :: String -> Either String Person
example ageString =
getAge ageString
>>= (\age -> checkAge age
>>= (\()-> createPerson age))
getAge :: Read b => String -> Either [Char] b
getAge ageString = case readMaybe ageString of
Nothing -> Left "Invalid age string"
Just age -> pure age
checkAge :: (Ord a, Num a) => a -> Either [Char] ()
checkAge a = if a < 0
then Left "Negative age"
else pure ()
createPerson :: Applicative f => Int -> f Person
createPerson a = pure Person { age = a }
I think this makes the 'trick' of passing the () through binds much more visible - the values are taken from an outer scope, while Left indeed short-circuits the processing.
It typechecks because
Either String bandEither a ()unify successfully, withString ~ aandb ~ ():It appears in the
doblock of typeEither String Person, so it's OK, since it's the same monad,Either, with the same "error signal" type,String.It appears in the middle of the
doblock, and there's no value "extraction". So it serves as a guard.It goes like this: if it was
Right y, then thedoblock's translation isand the computation continues inside
.....with theyvalue ignored. But if it wasLeft x, thenaccording to the definition of
>>=forEither. Crucially, theLeft xon the right is not the same value asLeft xon the left. The one on the left has typeEither String (); the one on the right has typeEither String Personindeed, as demanded by the return type of thedoblock overall.The two
Left xare two different values, each with its own specific type. Thex :: Stringis the same, of course.