Haskell 单子:Maybe 与 Either
在 Haskell 中处理可能失败的计算时,Maybe 和 Either 是两个最基础也最常用的单子(Monad)。它们能让你以声明式的方式组合可能出错的操作,避免层层嵌套的条件判断。下面通过具体步骤,手把手教你如何正确使用它们。
理解 Maybe:处理“有值”或“无值”
Maybe 类型只有两种可能:Just value 表示成功并携带一个值,Nothing 表示失败或缺失。
-
定义
Maybe类型:
在代码中,Maybe a表示一个可能是a类型值、也可能什么都没有的容器。例如:safeHead :: [a] -> Maybe a safeHead [] = Nothing safeHead (x:_) = Just x -
用
fmap对Maybe内的值做变换:
如果你有一个Maybe Int,想对其内部的数字加 1,但不确定它是不是Nothing,使用fmap:increment :: Maybe Int -> Maybe Int increment mx = fmap (+1) mx当
mx是Just 5时,结果是Just 6;当mx是Nothing时,结果仍是Nothing。 -
链式组合多个可能失败的操作:
使用>>=(bind 操作符)将多个返回Maybe的函数串起来。只要中间任何一步返回Nothing,整个链条立即短路返回Nothing。divide :: Int -> Int -> Maybe Int divide _ 0 = Nothing divide x y = Just (x `div` y) process :: Int -> Int -> Int -> Maybe Int process a b c = do x <- divide a b y <- divide x c return y等价于:
process a b c = divide a b >>= \x -> divide x c
理解 Either:携带错误信息的失败
Either 比 Maybe 更强大,因为它允许你在失败时附带一条错误消息(或其他错误类型)。
-
定义
Either类型:
Either e a有两种构造:Left error表示失败并携带错误信息e,Right value表示成功并携带值a。按照惯例,Left用于错误,Right用于正常结果。 -
编写返回
Either String a的函数:
用Left包装错误描述,用Right包装有效结果:safeDivide :: Int -> Int -> Either String Int safeDivide _ 0 = Left "Division by zero" safeDivide x y = Right (x `div` y) -
组合多个
Either操作:
和Maybe一样,使用do记法或>>=来串联:calculate :: Int -> Int -> Int -> Either String Int calculate a b c = do x <- safeDivide a b y <- safeDivide x c return y如果
b是 0,函数立即返回Left "Division by zero",不会尝试第二步除法。 -
统一错误类型以便组合:
如果不同函数返回不同错误类型(如Either String a和Either IOError a),先转换为同一错误类型再组合。常用方法是定义自己的错误类型:data AppError = DivByZero | FileNotFound deriving Show toAppError :: Either String a -> Either AppError a toAppError (Left _) = Left DivByZero toAppError (Right x) = Right x
对比 Maybe 与 Either 的适用场景
| 场景 | 推荐类型 | 原因 |
|---|---|---|
| 只关心“有没有结果”,不关心失败原因(如查找列表元素) | Maybe |
简洁,无需构造错误信息 |
| 需要向调用者说明失败原因(如解析配置、文件读取) | Either |
能携带具体错误描述,便于调试和用户提示 |
| 多个步骤可能失败,且希望提前终止并保留第一个错误 | Either |
Maybe 无法区分不同失败原因 |
实战:从字符串解析整数并做安全运算
假设你要写一个函数,接收两个字符串,尝试转为整数,然后相除。
-
使用
reads安全解析字符串:parseInt :: String -> Maybe Int parseInt s = case reads s of [(x, "")] -> Just x _ -> Nothing -
组合解析与除法(用
Maybe):parseAndDivide :: String -> String -> Maybe Int parseAndDivide s1 s2 = do x <- parseInt s1 y <- parseInt s2 if y == 0 then Nothing else Just (x `div` y) -
改用
Either提供详细错误:data ParseError = InvalidNumber String | ZeroDivision deriving Show parseIntE :: String -> Either ParseError Int parseIntE s = case reads s of [(x, "")] -> Right x _ -> Left (InvalidNumber s) parseAndDivideE :: String -> String -> Either ParseError Int parseAndDivideE s1 s2 = do x <- parseIntE s1 y <- parseIntE s2 if y == 0 then Left ZeroDivision else Right (x `div` y) -
测试你的函数:
- 调用
parseAndDivideE "10" "2"返回Right 5 - 调用
parseAndDivideE "abc" "2"返回Left (InvalidNumber "abc") - 调用
parseAndDivideE "10" "0"返回Left ZeroDivision
- 调用
避免常见误区
-
不要手动模式匹配代替单子操作:
写成case mx of Nothing -> ...; Just x -> ...会导致代码嵌套过深。优先使用do或>>=保持线性流程。 -
不要混淆
Either的左右顺序:
Either e a中,Left是错误,Right是成功。始终遵循这一约定,否则会破坏与标准库函数(如either)的兼容性。 -
不要在
Maybe中强行塞入错误信息:
如果你需要错误详情,直接用Either。试图用Maybe (Either err a)或类似结构只会让代码复杂化。 -
理解
fmap与>>=的区别:fmap f mx:对mx内部的值应用纯函数f,不改变容器结构。mx >>= f:将mx内部的值交给一个返回新容器的函数f,可能改变容器结构(如从Just x变成Nothing)。
扩展:使用 Control.Monad 提供的辅助函数
Haskell 标准库提供了许多简化 Maybe 和 Either 操作的工具。
-
用
when在Maybe上下文中执行副作用(仅当条件满足):import Control.Monad (when) printIfJust :: Maybe String -> IO () printIfJust msg = when (isJust msg) $ putStrLn (fromJust msg)(注意:
fromJust不安全,仅用于已知非Nothing的情况) -
用
maybe提供默认值:defaultValue :: Maybe Int -> Int defaultValue mx = maybe 0 id mx -- 若 mx 是 Nothing,返回 0;否则返回内部值 -
用
either处理Either的两种情况:handleResult :: Either String Int -> String handleResult = either id show -- Left 直接返回错误字符串,Right 转为数字字符串 -
用
catMaybes过滤并展开Maybe列表:validNumbers :: [String] -> [Int] validNumbers ss = catMaybes (map parseInt ss)
性能与实践建议
-
Maybe和Either都是惰性求值的:
它们不会立即计算内部值,直到被实际需要。这对性能通常有利,但要注意避免意外的空间泄漏。 -
在 API 设计中优先使用
Either:
对外暴露的函数若可能失败,返回Either err result比Maybe result更友好,因为调用者能获得明确的错误原因。 -
结合类型类约束提升泛化能力:
如果你的函数只依赖单子行为,用Monad m => ...作为类型签名,这样Maybe、Either甚至IO都能复用同一逻辑:sequenceSafeDiv :: Monad m => m Int -> m Int -> m Int sequenceSafeDiv mx my = do x <- mx y <- my if y == 0 then fail "zero" else return (x `div` y)(注意:
fail在Maybe中返回Nothing,在Either中需自定义实例) -
使用 newtype 包装
Either以启用更多实例:
例如,Validation(来自either库)允许累积多个错误,而标准Either只保留第一个错误。
-- 示例:累积所有解析错误
import Data.Either.Validation
parseIntV :: String -> Validation [String] Int
parseIntV s = case reads s of
[(x, "")] -> Success x
_ -> Failure ["Invalid number: " ++ s]
暂无评论,快来抢沙发吧!