monad和宏有什么区别?

Mai*_*tor 2 macros monads scheme haskell functional-programming

我读过一些monads tutoriais,他们几乎提出monad是实现操作顺序所必需的.但同样可以通过以下方式实现let:

(let*
    (   (a 1)
        (b (+ a 1))
        (c (+ b 1)))
    c)
Run Code Online (Sandbox Code Playgroud)

被禁止(实际上,我为了说明目的写了这个脑谜,请参阅Will Ness的评论):

((lambda (c) c)
    ((lambda (b) (+ b 1)) 
        ((lambda (a) (+ a 1))
            1)))
Run Code Online (Sandbox Code Playgroud)

那么如果这个宏能够实现排序,那么如何实现排序需要monad呢?

Dav*_*vid 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
Run Code Online (Sandbox Code Playgroud)

翻译这个变得棘手,因为Scheme是动态类型的.>>=return根据所涉及的类型有不同的行为.

一个简单的例子是Maybemonad,它在Scheme中看起来像这样:

(define (Just x)
    (cons 'Just x))
(define Nothing 'Nothing)

(define return Just)
(define (>>= m f)
   (if (eq? m 'Nothing)
       'Nothing
       (f (cdr m))))
Run Code Online (Sandbox Code Playgroud)

这可以被认为代表可能失败的计算.以下是Maybe monad的一些示例:

(>>= (Just 1) (lambda (x) (return (+ x 5))))   ==  '(Just . 6)
(>>= Nothing  (lambda (x) (return (* x 10))))  ==  'Nothing
(>>= (Just 5) (lambda (x) Nothing))            ==  'Nothing
Run Code Online (Sandbox Code Playgroud)

注意如果任何子Nothing计算结果如何,整个计算结果为Nothing.这是Maybemonad 的主要特征.

另一个常见的例子是列表monad,它可能看起来像

(define (return x) (list x))
(define (>>= xs f)
    (flatten (map f xs)))
Run Code Online (Sandbox Code Playgroud)

可以将此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)
Run Code Online (Sandbox Code Playgroud)

注意:在强烈尝试学习monad之前,我强烈建议学习仿函数(在Haskell中使用这个词的意义上)和applicative functors.这些概念相互依赖.


Aar*_*iel 5

(为清楚起见编辑,以更直接地解决问题.)

Monads和宏

Monads和宏是不同类型的东西.正如我将在下面展示的那样,Haskell 中Monad的Monad do 符号Identity类似于let-recdesugaring.然后我们将看到由于Haskell中的惰性评估,desugaring是如何不等同的.

如果您不想进一步阅读,摘要是宏重写语法树(在Lisps中),而在Haskell中,monad do-notation是语法糖.Monads本身只是具有相关函数字典的类型,因此Identitymonad 与orad不同,或者IOSTMor Either或不同Cont.

Monad-Do序列表达式吗?

首先,让我们假设Lisps和Haskell具有相同的执行策略(严格,按值调用).在这个假设中,Haskell的Monad do注释和平均Lisp 之间没有太大的区别 let-rec.对于我将要展示的所有Haskell,我将在顶部有这个import语句:

import Control.Monad.Identity    
Run Code Online (Sandbox Code Playgroud)

因此,请考虑以下功能:

f :: Identity Int 
f = do
  a <- return 1
  b <- return (a + 1)
  c <- return (b + 1)
  return c
Run Code Online (Sandbox Code Playgroud)

我正在使用Identity monad,其行为最相似let-rec.这个do符号将会出现这种情况:

fDesugar :: Identity Int
fDesugar =
  (return 1 >>= \a -> 
    (return (a + 1) >>= \b ->
      (return (b + 1) >>= \c ->
        return c)))
Run Code Online (Sandbox Code Playgroud)

这看起来像你的Lisp示例,但显然它使用的是中缀表示法,这会产生不同的参数序列.我们可以将它重写为Lisp,如:

fLisp :: Identity Int
fLisp =
  ((=<<) (\c -> return c)
    ((=<<) (\b -> return (b + 1))
      ((=<<) (\a -> return (a + 1))
        (return 1))))
Run Code Online (Sandbox Code Playgroud)

现在,我们可以使用"运行"这个monad runIdentity.那是什么意思?为什么我们需要"运行"monad?

使用Monads

Monads在类型的构造函数上定义.有些教程将它们描述为burrito包装器,但我只想说monad可以在很多类型上定义 - 那些类型.monad的一个例子是我已经使用过的 Identity,另一个是monad IO.这些类型中的每一个实际上都是一种类型的值* -> *,也就是说,它采用一种类型(种类*)并返回一种新类型.所以一个单子为定义Identity,其然后可以用使用Identity Int, Identity String,Identity Foo,等.

因为monad是按类型定义,所以在评估某些类型时 Identity T,我们需要知道如何执行该monad.可以说,排序取决于类型.

身份是一个非常简单的构造函数和monad.构造函数是:

newtype Identity a = Identity { runIdentity :: a }
Run Code Online (Sandbox Code Playgroud)

也就是说,要构造一个类型的值Identity T,我们只需要传入一个 T.为了解决它,我们只是申请runIdentity它.那是:

makeIdentity :: a -> Identity a
makeIdentity a = Identity a

runIdentity :: Identity a -> a
runIdentity (Identity a) = a
Run Code Online (Sandbox Code Playgroud)

要理解fLisp,fDesugar或者f,我们需要知道monad 语境中的含义>>=return含义Identity:

instance Monad Identity where
    return a = Identity a
    m >>= k  = k (runIdentity m)
Run Code Online (Sandbox Code Playgroud)

有了这些知识,我们可以推导出=<<身份:

    k =<< m = k (runIdentity m)
Run Code Online (Sandbox Code Playgroud)

有了这个,我们可以fLisp使用我们的定义重写.在Haskell的说法中,我们使用的是等式推理.我们可以用右侧替换上面定义的左侧.因此,我们替换 (=<<) a ba (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)))))))
Run Code Online (Sandbox Code Playgroud)

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)))))
Run Code Online (Sandbox Code Playgroud)

我们终于可以Identity拿出第一个函数的最后一个了,我们得到:

fLisp''' :: Identity Int
fLisp''' =
  Identity 
    ((\c -> c)
      (((\b -> (b + 1))
        (((\a -> (a + 1))
          1)))))
Run Code Online (Sandbox Code Playgroud)

所以,显然Identity表现得一样let-rec,对吧?

莫纳德不同的地方

单子每个类型定义的,所以Identity是很多像您的宏let-rec,但其中两个在一个关键的方式不同的是,哈斯克尔没有表现为我们承担.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
Run Code Online (Sandbox Code Playgroud)

five评估时,结果确实是五.尽管它来自污染的价值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
Run Code Online (Sandbox Code Playgroud)

奇怪的是,runIdentity g回报3.同样的runIdentity f.

Monads不是宏,甚至Identitymonad 都没有复制行为 let-rec.我在Racket中尝试了这个,我收到了一个错误.我甚至无法编译程序!定义g评估错误代码.

(define g 
    (letrec
      (   (a 1)
          (b (+ a 1))
          (c (+ b 1))
          (d (error "I will crash Racket!"))
    ) c))
Run Code Online (Sandbox Code Playgroud)

Monad为何不同?

在Haskell中,值在其参数中是惰性的.如果该对的第二个值从未被强制(必需),那么Haskell将不会爆炸.并且fst f执行起来是"安全的".在Lisp中,值是严格的,并且g总是会爆炸.

由于Haskell的懒惰,许多作者将手动波单调作为排序操作的唯一方式.这不是严格意义上的!(没有双关语.)

单子的话,不只是宏let-rec,因为定义 >>=return可这样变化很大.我们可以用Eithermonad 表示潜在的失败计算.

我倾向于将monad视为排序操作的方式,但更像是用户定义的"分号"运算符.在一些monads(IO)中,绑定行为很像C或C++中的分号.在其他monad中,分号(绑定)执行其他操作,例如将错误条件链接在一起或修改控制流.

请参阅monad的此示例Cont供参考.请注意,在这种情况下,在Cont monad中,可以通过do块内的语句更改执行流程.


Sas*_* NF 5

Monad不是一个宏.Monad是两个函数的存在,它们之间具有特定的对应关系.所以它并没有消除lambda,而是指定了对它们的限制.

我不会说monads"序列"操作.它并不总是他们所做的.A (r->a)是monad in a,它当然不会"排序"任何东西.A ((a->r)->a)也是Monad a.

我建议理解monad的重点应该是理解这两个操作和法律的含义.通常教程会谈论return>>=,但顿悟是在理解<=<,可以用来表达>>=.

在"普通"类型中,您将组合定义为:

(.) :: (b->c) -> (a->b) -> (a->c)
Run Code Online (Sandbox Code Playgroud)

然后写

f . g
Run Code Online (Sandbox Code Playgroud)

使用monad,您可以将组合定义为:

(<=<) :: (Monad m) => (b->m c) -> (a->m b) -> (a->m c)
Run Code Online (Sandbox Code Playgroud)

然后写

f <=< g
Run Code Online (Sandbox Code Playgroud)