(为清楚起见进行了编辑以更直接地解决问题。)
单子和宏
Monad 和宏是不同的东西。正如我将在下面展示的,monad do
Haskell 中 Identity monad 的符号类似于 let-rec
脱糖。然后我们将看到由于懒惰,脱糖是如何不等价的
在 Haskell 中进行评估。
如果您不想进一步阅读,摘要是宏重写
语法树(在 Lisps 中),在 Haskell 中,monad do-notation 是语法
糖。单子本身只是具有相关字典的类型
函数,所以Identity monad 与IO 不同,这不是
与STM 或Either 或Cont 相同。
Monad-Do 序列表达式吗?
首先,假设 Lisp 和 Haskell 具有相同的执行策略(严格,
按值调用)。在这个假设下,两者没有太大区别
Haskell 的 Monad do-notation 和 Lisp 的平均 let-rec。对于所有
我将要展示的 Haskell,我将在
顶部:
import Control.Monad.Identity
所以,考虑以下函数:
f :: Identity Int
f = do
a <- return 1
b <- return (a + 1)
c <- return (b + 1)
return c
我正在使用 Identity monad,它的行为与 let-rec 最相似。
do 符号将对此进行脱糖:
fDesugar :: Identity Int
fDesugar =
(return 1 >>= \a ->
(return (a + 1) >>= \b ->
(return (b + 1) >>= \c ->
return c)))
这看起来像您的 Lisp 示例,但显然它使用的是中缀表示法和
这会导致不同的参数序列。我们可以把它改写成 Lisp
喜欢:
fLisp :: Identity Int
fLisp =
((=<<) (\c -> return c)
((=<<) (\b -> return (b + 1))
((=<<) (\a -> return (a + 1))
(return 1))))
现在,我们可以使用 runIdentity“运行”这个 monad。那是什么意思?为什么我们
需要“运行”单子?
使用单子
Monad 是在类型的构造函数上定义的。一些教程将它们描述为
墨西哥卷饼包装,但我只想说一个单子可以定义很多
采取类型的类型。 monad 的一个例子是我已经用过的那个,
Identity,另一个单子是IO。这些类型中的每一个实际上都是一个值
kind * -> *,也就是说,它接受一个类型(类型为*)并返回一个新类型。所以
为Identity 定义了一个monad,然后可以与Identity Int 一起使用,
Identity String、Identity Foo 等等。
因为 monad 是 按类型定义的,所以在评估类型时
Identity T,我们需要知道如何执行那个 monad。排序,所以
说话,取决于类型。
Identity 是一个非常简单的构造函数和 monad。构造函数是:
newtype Identity a = Identity { runIdentity :: a }
也就是说,要构造一个Identity T类型的值,我们只需要传入一个
T。为了让它出来,我们只需将runIdentity 应用于它。那就是:
makeIdentity :: a -> Identity a
makeIdentity a = Identity a
runIdentity :: Identity a -> a
runIdentity (Identity a) = a
要了解fLisp、fDesugar 或f,我们需要知道>>= 和
return 的意思是在 Identity monad 的上下文中:
instance Monad Identity where
return a = Identity a
m >>= k = k (runIdentity m)
有了这些知识,我们可以推导出 =<< 来表示身份:
k =<< m = k (runIdentity m)
有了这个,我们可以使用我们的定义重写fLisp。在哈斯克尔
用语,我们正在使用等式推理。我们可以用左手代替
我们上面定义的一侧与右侧。所以我们替换
(=<<) a b 和 a (runIdentity b),return a 和 Identity a:
fLisp' :: Identity Int
fLisp' =
((\c -> Identity c)
(runIdentity ((\b -> Identity (b + 1))
(runIdentity ((\a -> Identity (a + 1))
(runIdentity (Identity 1)))))))
但是runIdentity 只是将Identity 从它所应用的任何内容中剥离出来。什么时候
runIdentity 应用于以下形式:
runIdentity ((\a -> Identity (f a)) b) 我们可以将它移动到它的参数中:
(\a -> runIdentity (Identity a)) b,并将其减少到 (\a -> f a) b 或只是
f a。让我们完成所有这些步骤:
fLisp'' :: Identity Int
fLisp'' =
((\c -> Identity c)
(((\b -> (b + 1))
(((\a -> (a + 1))
1)))))
我们终于可以从第一个函数中取出最后一个Identity,我们得到:
fLisp''' :: Identity Int
fLisp''' =
Identity
((\c -> c)
(((\b -> (b + 1))
(((\a -> (a + 1))
1)))))
那么,显然Identity 的行为与let-rec 相同,对吧?
Monad 的不同之处
Monad 是按类型定义的,所以 Identity 很像你的宏 let-rec,
但两者在关键方面的不同之处在于 Haskell 不表现得像
我们假设。 Haskell 不执行严格的按值调用求值。
我们可以通过在 Haskell 中编写来证明这一点。 blowUp 是原始错误类型,当
评估它会冒泡一个异常并结束执行。
blowUp :: a
blowUp = error "I will crash Haskell!"
riskyPair :: (Int, a)
riskyPair = (5, blowUp)
fst' :: (a, b) -> a
fst' = \(a, b) -> a
five :: Int
five = fst' riskyPair
当评估five 时,结果确实是5。尽管它来自
污染值g,我们能够安全地评估fst' riskyPair,而无需
吹起来。不过尝试评估riskyPair,您会看到异常。
考虑下面的函数g,它使用了Identity monad,但是什么
可以吗?
g :: Identity Int
g = do
a <- return 1
b <- return (a + 1)
c <- return (b + 1)
d <- return (blowUp)
return c
奇怪的是,runIdentity g 返回3。与runIdentity f 相同。
Monad 不是宏,甚至 Identity monad 也不会复制
let-rec。我在 Racket 中试过这个,但我得到了一个错误。我什至无法编译
该程序!定义 g 评估错误代码。
(define g
(letrec
( (a 1)
(b (+ a 1))
(c (+ b 1))
(d (error "I will crash Racket!"))
) c))
为什么 Monad 不同?
在 Haskell 中,值的参数是惰性的。如果该对的第二个值
永远不会被强迫(必需),那么 Haskell 就不会爆炸。而fst f 是“安全的”
执行。在 Lisp 中,值是严格的,g 总是会爆炸。
由于 Haskell 的懒惰,许多作者将手挥 monad 作为唯一的
排序操作的方式。严格来说,这不是真的! (没有双关语。)
那么,Monad 不仅仅是 let-rec 的宏,因为对于
>>= 和 return 变化如此之大。我们可以代表潜在的失败
使用Either monad 进行计算。
我倾向于认为 monad 不是排序操作的方式,而更像是
用户定义的“分号”运算符。在某些 monads (IO) 中,绑定的行为是
很像 C 或 C++ 中的分号。在其他单子中,分号(绑定)确实
其他事情,例如将错误条件链接在一起或修改控制流。
见this example
Cont monad 供参考。请注意,在这种情况下,在 Cont
monad,可以通过 do 块内的语句来改变执行流程。