monad 初体验 (Haskell)

First experience with monads (Haskell)

对于主题不清楚,我深表歉意。我是 Haskell 99 初学者,在 the solution to 67A 中遇到了人生中第一次遇到 monad 的概念。我正在努力解决的问题的一部分是定义一个函数 stringToTreea(b,c) 之类的序列转换为 TreeBranch a (Branch b Empty Empty) (Branch c Empty Empty).

我已经尝试了几次 "soft" 对 monad 的介绍,但都像其他许多人一样失败了。我希望通过理解这个解决方案最终将我带入室内,所以我决定在这里试一试。

问题

  1. 谁能简单解释一下解决方案中定义的函数stringToTree :: (Monad m) => String -> m (Tree Char)是什么?为了使这个问题独立,我从那里复制了代码
stringToTree :: (Monad m) => String -> m (Tree Char)
stringToTree "" = return Empty
stringToTree [x] = return $ Branch x Empty Empty
stringToTree str = tfs str >>= \ ("", t) -> return t
    where tfs a@(x:xs) | x == ',' || x == ')' = return (a, Empty)
          tfs (x:y:xs)
                | y == ',' || y == ')' = return (y:xs, Branch x Empty Empty)
                | y == '(' = do (',':xs', l) <- tfs xs
                                (')':xs'', r) <- tfs xs'
                                return $ (xs'', Branch x l r)
          tfs _ = fail "bad parse"
  1. 为什么 monad 在这里有用?我希望看到 monads 如何大大减少困难,当然只有在理解它之后,同时定义这个函数。

简而言之,定义让调用者选择使用哪个monad。这让我们可以定制我们处理失败的方式。例如,我们可以使用 Maybe:

>>> stringToTree "" :: Maybe (Tree Char)
Just Empty
>>> stringToTree "9 +" :: Maybe (Tree Char)
Nothing

[]:

>>> stringToTree "" :: [Tree Char]
[Empty]
>>> stringToTree "9 +" :: [Tree Char]
[]

代码本身不假设使用了哪个 monad;它仅使用 >>=returnfail 来处理递归调用的结果。

将类型设置为String -> Tree Char将表明失败根本不可能发生; 每个 字符串都必须产生一个有效的Tree Char 值(否则我们会引发运行时错误,这是您在Haskell 中应该努力避免的事情)。

但请注意,并非所有 monad 都提供 fail 的定义来避免运行时错误。

>>> stringToTree "" :: Either () (Tree Char)
Right Empty
>>> stringToTree "9 +" :: Either () (Tree Char)
*** Exception: bad parse

为什么 monad 有用?一些更广泛的背景。

Monad 是传统上由不同语言机制处理的几种事物的统一。其中有

  • 排序(如在 IO 或状态中)

  • 非确定性(如列表 monad)

  • 失败和异常(如 Maybe)

  • 保存和恢复计算(以后会遇到的Cont monad)

  • 原子事务(STM monad)

  • 解析(Parsec 及其亲戚)

"unification"我指的是牛顿统一地球和天空的物理定律,或者麦克斯韦将电和磁统一到电磁场时所做的事情;它是一个更深层次的基础理论,将上述所有情况作为特例产生,并展示了如何按照相同的思路创建新事物。

在单子中,"bind" 函数的类型 (>>=) 是理论中的中心方程;它描述了如何将计算中的一个步骤以菊花链方式连接到下一个步骤。 return 是原始的空步骤。这是你在 Haskell 中习惯的东西;一切都有某种零或空或身份价值。 fail 是一个不起作用的步骤,部分函数需要它(正如@chepner 在其他地方的评论中所说)。