我读过一些monad tutoriais,他们几乎提出monad是实现操作顺序所必需的。但let
:
(let*
( (a 1)
(b (+ a 1))
(c (+ b 1)))
c)
是否已经过去了(实际上并没有,为了说明目的,我写了这个脑谜,请参阅Will Ness的评论):
((lambda (c) c)
((lambda (b) (+ b 1))
((lambda (a) (+ a 1))
1)))
那么如果这个宏能够实现排序,那么如何实现排序呢?
答案 0 :(得分:7)
monad只是一种类型定义的一组法则。以下是Scheme符号中的定律(return
和>>=
是monadic函数):
(>>= (return a) k) == (k a) ; Left identity
(>>= m return) == m ; Right identity
(>>= m (lambda (x) (>>= (k x) h))) == (>>= (>>= m k) h) ; Associativity
翻译这个变得棘手,因为Scheme是动态类型的。 >>=
和return
具有不同的行为,具体取决于所涉及的类型。
一个简单的例子是Maybe
monad,它在Scheme中看起来像这样:
(define (Just x)
(cons 'Just x))
(define Nothing 'Nothing)
(define return Just)
(define (>>= m f)
(if (eq? m 'Nothing)
'Nothing
(f (cdr m))))
这可以被认为代表可能失败的计算。以下是Maybe monad的一些例子:
(>>= (Just 1) (lambda (x) (return (+ x 5)))) == '(Just . 6)
(>>= Nothing (lambda (x) (return (* x 10)))) == 'Nothing
(>>= (Just 5) (lambda (x) Nothing)) == 'Nothing
请注意,如果任何子计算导致Nothing
,则整个计算结果为Nothing。这是Maybe
monad的主要特征。
另一个常见的例子是列表monad,它看起来像
(define (return x) (list x))
(define (>>= xs f)
(flatten (map f xs)))
这个monad可以被认为是一个非确定性计算(也就是说,一个可能具有几个可能值的计算)。以下是一些例子:
(>>= '(1 2 3) (lambda (x) (return (* x 10)))) == '(10 20 30)
(>>= '(5 6 7) (lambda (x) (list (* x 10) (+ x 2)))) == '(50 7 60 8 70 9)
注意强> 的: 我强烈建议在真正尝试学习monad之前学习仿函数(在Haskell中使用这个词的意义上)和applicative functors。这些概念相互依存。
答案 1 :(得分:5)
(为了更清楚地编辑而更直接地解决了这个问题。)
Monads和宏是不同类型的东西。正如我在下面所示,monad do
Haskell中Identity
monad的表示法类似于let-rec
脱糖。然后,我们将看到由于懒惰,desugaring是如何不等同的
在Haskell进行评估。
摘要,如果你不在乎进一步阅读,那就是宏重写了
语法树(在Lisps中),在Haskell中,monad do
- 表示法是语法
糖。 Monads本身只是具有相关词典的类型
函数,因此Identity
monad与IO
不同,而不是STM
与Either
或Cont
或Monad
相同。
首先,让我们假设Lisps和Haskell具有相同的执行策略(严格的,
按价值呼叫)。在这个假设中,两者之间没有太大区别
Haskell的do
let-rec
- 符号和普通的Lisp' s import Control.Monad.Identity
。对于所有人
Haskell我将要展示,我将在此处发表此导入声明
顶部:
f :: Identity Int
f = do
a <- return 1
b <- return (a + 1)
c <- return (b + 1)
return c
因此,请考虑以下功能:
let-rec
我正在使用Identity monad,其行为与do
的行为最相似。
fDesugar :: Identity Int
fDesugar =
(return 1 >>= \a ->
(return (a + 1) >>= \b ->
(return (b + 1) >>= \c ->
return c)))
符号会出现这种情况:
fLisp :: Identity Int
fLisp =
((=<<) (\c -> return c)
((=<<) (\b -> return (b + 1))
((=<<) (\a -> return (a + 1))
(return 1))))
这看起来像你的Lisp示例,但显然它使用的是中缀表示法 这导致了不同的参数序列。我们可以将它重写为Lisp 像:
runIdentity
现在,我们可以&#34;运行&#34;这个monad使用Identity
。那是什么意思?为什么我们
需要&#34;运行&#34;单子?
Monad是在类型的构造函数上定义的。一些教程将它们描述为
墨西哥卷饼包装,但我只是说可以定义一个monad
类型 - 采取类型。 monad的一个例子就是我已经使用过的那个,
IO
,另一个monad是* -> *
。这些类型中的每一个实际上都是值
kind *
,也就是说,它需要一种类型(种类Identity
)并返回一个新类型。所以
为Identity Int
定义了一个monad,然后可以与Identity String
一起使用,
Identity Foo
,Identity T
等等。
因为在评估类型的某些内容时,monad被定义为 per-type
newtype Identity a = Identity { runIdentity :: a }
,我们需要知道如何执行该monad。顺序,所以
说话,取决于类型。
Identity是一个非常简单的构造函数和monad。构造函数是:
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
表示instance Monad Identity where
return a = Identity a
m >>= k = k (runIdentity m)
monad的上下文:
=<<
有了这些知识,我们可以为身份派生 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
的行为与Identity
相同,对吧?
Monads是按类型定义的,因此let-rec
与您的宏blowUp
非常相似,
但是两者在一个关键方面不同的地方是Haskell 不表现为
我们假设。 Haskell没有执行严格的按值调用的评估。
我们可以通过在Haskell中编写来证明这一点。 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
评估g
时,结果确实是五。尽管它来自于
受污染的值fst' riskyPair
,我们可以安全地评估riskyPair
吹起来。尝试评估g
,然后您会看到异常。
考虑以下函数g :: Identity Int
g = do
a <- return 1
b <- return (a + 1)
c <- return (b + 1)
d <- return (blowUp)
return c
,它使用Identity monad,但是它是什么
它呢?
runIdentity g
奇怪的是,3
会返回runIdentity f
。与Identity
相同。
Monads不是宏,甚至let-rec
monad也没有重复行为
g
。我在Racket中尝试了这个,我收到了一个错误。我甚至无法编译
该程序!定义(define g
(letrec
( (a 1)
(b (+ a 1))
(c (+ b 1))
(d (error "I will crash Racket!"))
) c))
评估了错误代码。
fst f
在Haskell中,值在参数中是懒惰的。如果该对的第二个值
永远不会强制(必需)然后Haskell不会爆炸。 g
是&#34;安全&#34;
执行。在Lisp中,值是严格的,let-rec
总会爆炸。
由于Haskell的懒惰,许多作者将手动挥动monad作为唯一 排序操作的方式。这不是严格意义上的! (没有双关语。)
Monads,不仅仅是>>=
的宏,因为它的定义
return
和Either
变化如此之大。我们可能代表潜在的失败
使用IO
monad进行计算。
我倾向于认为monads不是测序操作的方法,而是更像是a
用户定义的&#34;分号&#34;运营商。在一些monad(Cont
)中,绑定行为是
很像C或C ++中的分号。在其他monad中,分号(绑定)可以
其他的事情,如链接错误条件或修改控制流程。
参见this example
Cont
monad的参考。请注意,在这种情况下,在do
内
monad,执行流程可以通过{{1}}块内的语句来改变。
答案 2 :(得分:5)
Monad不是一个宏。 Monad是两个函数的存在,它们之间具有特定的对应关系。所以它并没有消除lambda,而是指定了对它们的限制。
我不会说monad“序列”操作。它并不总是他们所做的。 (r->a)
是a
中的monad,它肯定不会“排序”任何内容。 ((a->r)->a)
也是a
中的Monad。
我建议理解monad的重点应该是理解这两个操作和法律的含义。通常,教程会讨论return
和>>=
,但顿悟是理解<=<
,可以用来表达>>=
。
在“普通”类型中,您将组合定义为:
(.) :: (b->c) -> (a->b) -> (a->c)
然后写
f . g
使用monads,将composition定义为:
(<=<) :: (Monad m) => (b->m c) -> (a->m b) -> (a->m c)
然后写
f <=< g
答案 3 :(得分:0)
我认为值得在这里添加一个细微之处。
monad设计模式和宏提供相似的结果,我认为这导致了这个问题。
Monad提供了一种提取重复代码的方法,并能够根据大多数FP语言中的某些条件进行分支,此外还提供了另一种方法,即通过将一些基础类型包装在我们可以控制的上下文中来对两个函数进行排序。
但是,在lisps中,我们不需要它,因为宏功能强大得多,并提供相同或更多的功能。我们可以根据需要重新排列代码。因此,我们在Clojure中使用了线程宏,cond宏,let宏等。
因此,如我们所见,合法的或非法的monad在具有有限代码操纵支持的语言中非常有用,但是如果您使用的是lispy宏,则通常会更有用。