文章目录

Haskell 单子:Maybe 与 Either

发布于 2026-04-03 00:32:50 · 浏览 8 次 · 评论 0 条

Haskell 单子:Maybe 与 Either

在 Haskell 中处理可能失败的计算时,MaybeEither 是两个最基础也最常用的单子(Monad)。它们能让你以声明式的方式组合可能出错的操作,避免层层嵌套的条件判断。下面通过具体步骤,手把手教你如何正确使用它们。


理解 Maybe:处理“有值”或“无值”

Maybe 类型只有两种可能:Just value 表示成功并携带一个值,Nothing 表示失败或缺失。

  1. 定义 Maybe 类型
    在代码中,Maybe a 表示一个可能是 a 类型值、也可能什么都没有的容器。例如:

    safeHead :: [a] -> Maybe a
    safeHead []    = Nothing
    safeHead (x:_) = Just x
  2. fmap Maybe 内的值做变换
    如果你有一个 Maybe Int,想对其内部的数字加 1,但不确定它是不是 Nothing使用 fmap

    increment :: Maybe Int -> Maybe Int
    increment mx = fmap (+1) mx

    mxJust 5 时,结果是 Just 6;当 mxNothing 时,结果仍是 Nothing

  3. 链式组合多个可能失败的操作
    使用 >>=(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:携带错误信息的失败

EitherMaybe 更强大,因为它允许你在失败时附带一条错误消息(或其他错误类型)。

  1. 定义 Either 类型
    Either e a 有两种构造:Left error 表示失败并携带错误信息 eRight value 表示成功并携带值 a。按照惯例,Left 用于错误,Right 用于正常结果。

  2. 编写返回 Either String a 的函数
    Left 包装错误描述 Right 包装有效结果

    safeDivide :: Int -> Int -> Either String Int
    safeDivide _ 0 = Left "Division by zero"
    safeDivide x y = Right (x `div` y)
  3. 组合多个 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",不会尝试第二步除法。

  4. 统一错误类型以便组合
    如果不同函数返回不同错误类型(如 Either String aEither 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 无法区分不同失败原因

实战:从字符串解析整数并做安全运算

假设你要写一个函数,接收两个字符串,尝试转为整数,然后相除。

  1. 使用 reads 安全解析字符串

    parseInt :: String -> Maybe Int
    parseInt s = case reads s of
      [(x, "")] -> Just x
      _         -> Nothing
  2. 组合解析与除法(用 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)
  3. 改用 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)
  4. 测试你的函数

    • 调用 parseAndDivideE "10" "2" 返回 Right 5
    • 调用 parseAndDivideE "abc" "2" 返回 Left (InvalidNumber "abc")
    • 调用 parseAndDivideE "10" "0" 返回 Left ZeroDivision

避免常见误区

  1. 不要手动模式匹配代替单子操作
    写成 case mx of Nothing -> ...; Just x -> ... 会导致代码嵌套过深。优先使用 do >>= 保持线性流程

  2. 不要混淆 Either 的左右顺序
    Either e a 中,Left 是错误,Right 是成功。始终遵循这一约定,否则会破坏与标准库函数(如 either)的兼容性。

  3. 不要在 Maybe 中强行塞入错误信息
    如果你需要错误详情,直接用 Either。试图用 Maybe (Either err a) 或类似结构只会让代码复杂化。

  4. 理解 fmap >>= 的区别

    • fmap f mx:对 mx 内部的值应用纯函数 f,不改变容器结构。
    • mx >>= f:将 mx 内部的值交给一个返回新容器的函数 f,可能改变容器结构(如从 Just x 变成 Nothing)。

扩展:使用 Control.Monad 提供的辅助函数

Haskell 标准库提供了许多简化 MaybeEither 操作的工具。

  1. when Maybe 上下文中执行副作用(仅当条件满足):

    import Control.Monad (when)
    
    printIfJust :: Maybe String -> IO ()
    printIfJust msg = when (isJust msg) $ putStrLn (fromJust msg)

    (注意:fromJust 不安全,仅用于已知非 Nothing 的情况)

  2. maybe 提供默认值

    defaultValue :: Maybe Int -> Int
    defaultValue mx = maybe 0 id mx  -- 若 mx 是 Nothing,返回 0;否则返回内部值
  3. either 处理 Either 的两种情况

    handleResult :: Either String Int -> String
    handleResult = either id show  -- Left 直接返回错误字符串,Right 转为数字字符串
  4. catMaybes 过滤并展开 Maybe 列表

    validNumbers :: [String] -> [Int]
    validNumbers ss = catMaybes (map parseInt ss)

性能与实践建议

  1. Maybe Either 都是惰性求值的
    它们不会立即计算内部值,直到被实际需要。这对性能通常有利,但要注意避免意外的空间泄漏。

  2. 在 API 设计中优先使用 Either
    对外暴露的函数若可能失败,返回 Either err result Maybe result 更友好,因为调用者能获得明确的错误原因。

  3. 结合类型类约束提升泛化能力
    如果你的函数只依赖单子行为, Monad m => ... 作为类型签名,这样 MaybeEither 甚至 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)

    (注意:failMaybe 中返回 Nothing,在 Either 中需自定义实例)

  4. 使用 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]

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文