这个技巧如何进行类型检查?

How does this trick type-check?

阅读此博客 post – https://www.haskellforall.com/2021/05/the-trick-to-avoid-deeply-nested-error.html – 我意识到我不明白为什么 'trick' 在这种情况下实际上有效:

{-# 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 }

具体来说,我很难理解为什么会这样

    if age < 0
        then Left "Negative age"
        else pure ()

类型检查。

Left "Negative age" 的类型为 Either String b

pure () 的类型是 Either a ()

为什么会这样?


编辑:我将代码简化并重新编写为绑定操作而不是 do 块,然后看到 Will 对他已经很出色的答案的编辑:

{-# 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 }

我认为这使得通过绑定传递 () 的 'trick' 更加明显 - 值取自外部范围,而 Left 确实使处理短路.

它进行类型检查,因为 Either String bEither a () 成功统一,String ~ ab ~ ():

     Either String b
     Either a      ()
   ------------------
     Either String ()      a ~ String, b ~ ()

它出现在类型Either String Persondo块中,所以没关系,因为它是同一个monad,Either,具有相同的“错误信号”类型,String.

出现在do块的中间,没有值“提取”。所以它起到了看守的作用。

它是这样的:如果它是 Right y,那么 do 块的翻译是

      Right y >>= (\ _ -> .....)

并在 ..... 内继续计算,忽略 y 值。但是如果是Left x,那么

      Left x >>= _  =  Left x

根据 >>=Either 的定义。至关重要的是,右边的 Left x 与左边的 Left x 而不是 相同的值。左侧的类型为 Either String ();右边的确实是 Either String Person 类型,正如整个 do 块的 return 类型所要求的那样。

两个Left x两个不同的值,每个都有自己特定的类型。 x :: String当然是一样的。