【问题标题】:What is the difference between monads and macros?单子和宏有什么区别?
【发布时间】:2014-03-24 10:23:49
【问题描述】:

我已经阅读了一些 monadstutorialis,他们几乎都认为 monads 是实现操作排序所必需的。但是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)))

那么,如果这个宏已经可以做到,那么为什么需要 monad 来实现排序呢?

【问题讨论】:

  • 宏更类似于惰性求值。宏可用于实现 monadic 接口的语法糖,但 monad 只是类型的一组规则(您可以在此处看到 hackage.haskell.org/package/base-4.6.0.1/docs/… 。还有许多不同的 monad。此外,我不认为是什么您所描述的实际上是一个 monad,而只是普通的标识符绑定。
  • 宏是 AST 转换器。您可以使用 TemplateHaskell 处理 AST,但它们与 monad 没有任何关系。
  • 您的宏对任何内容进行排序的唯一原因是因为在评估应用程序之前对过程的参数进行了评估。我对您的示例感到困惑,因为您谈论的排序只有在您谈论副作用时才真正相关,而您的代码没有。即使您确实使用 lambdas 来尝试对事物进行排序,这样做也是不好的做法,因为它假设了一些关于如何实现评估的事情。在 Scheme 中,您可以使用 begin 表单。
  • 排序let 通常称为let*,而不是letrec,它允许我们编写例如(let* ( (a 1) (b (+ a 1)) (c (+ b a))) c) 您提出的脱糖方案非常不寻常,它不会起作用,因为没有范围嵌套,即没有对相同变量的共享访问:((lambda (c) c) ((lambda (b) (+ b a)) #| doesn't work: what `a`? |# ((lambda (a) (+ a 1)) 1)))let* 的通常翻译是((lambda (a) ((lambda (b) ((lambda (c) c) (+ b a)) (+ a 1)) 1),其中每个 var 的作用域都是嵌套的,因此内部表达式可以访问外部 var。

标签: haskell macros functional-programming scheme monads


【解决方案1】:

monad 只是定义在类型上的一组法则。以下是 Scheme 表示法中的规律(return>>= 是一元函数):

(>>= (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 的主要特性。

另一个常见的例子是 list 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 之前先学习 functors(在 Haskell 中使用这个词的意义上)和 applicative functors。这些概念相互依存。

【讨论】:

    【解决方案2】:

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

    我不会说单子“序列”操作。他们并不总是这样做。 (r->a)a 中的 monad,它当然不会“排序”任何东西。 ((a->r)->a) 也是 a 中的 Monad。

    我建议理解 monad 的重点应该真正放在理解这两个操作的含义和规律上。通常教程讲的是return>>=,但顿悟在理解<=<,可以用来表达>>=

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

    (.) :: (b->c) -> (a->b) -> (a->c)
    

    然后写

    f . g
    

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

    (<=<) :: (Monad m) => (b->m c) -> (a->m b) -> (a->m c)
    

    然后写

    f <=< g
    

    【讨论】:

    • @WillNess 谢谢。添加了说明。它是搜索单子。
    【解决方案3】:

    (为清楚起见进行了编辑以更直接地解决问题。)

    单子和宏

    Monad 和宏是不同的东西。正如我将在下面展示的,monad do Haskell 中 Identity monad 的符号类似于 let-rec 脱糖。然后我们将看到由于懒惰,脱糖是如何不等价的 在 Haskell 中进行评估。

    如果您不想进一步阅读,摘要是宏重写 语法树(在 Lisps 中),在 Haskell 中,monad do-notation 是语法 糖。单子本身只是具有相关字典的类型 函数,所以Identity monad 与IO 不同,这不是 与STMEitherCont 相同。

    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 * -&gt; *,也就是说,它接受一个类型(类型为*)并返回一个新类型。所以 为Identity 定义了一个monad,然后可以与Identity Int 一起使用, Identity StringIdentity 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
    

    要了解fLispfDesugarf,我们需要知道&gt;&gt;=return 的意思是在 Identity monad 的上下文中:

    instance Monad Identity where
        return a = Identity a
        m >>= k  = k (runIdentity m)
    

    有了这些知识,我们可以推导出 =&lt;&lt; 来表示身份:

        k =<< m = k (runIdentity m)
    

    有了这个,我们可以使用我们的定义重写fLisp。在哈斯克尔 用语,我们正在使用等式推理。我们可以用左手代替 我们上面定义的一侧与右侧。所以我们替换 (=&lt;&lt;) a ba (runIdentity b)return aIdentity 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 -&gt; Identity (f a)) b) 我们可以将它移动到它的参数中: (\a -&gt; runIdentity (Identity a)) b,并将其减少到 (\a -&gt; 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 的宏,因为对于 &gt;&gt;=return 变化如此之大。我们可以代表潜在的失败 使用Either monad 进行计算。

    我倾向于认为 monad 不是排序操作的方式,而更像是 用户定义的“分号”运算符。在某些 monads (IO) 中,绑定的行为是 很像 C 或 C++ 中的分号。在其他单子中,分号(绑定)确实 其他事情,例如将错误条件链接在一起或修改控制流。

    this example Cont monad 供参考。请注意,在这种情况下,在 Cont monad,可以通过 do 块内的语句来改变执行流程。

    【讨论】:

    • 我遇到了this great description about a monads,在我看来,monads 是一个抽象概念,但宏是一个实现。所以比较单子和宏是一个失败的想法,这是完全不同的事情。 IE。在我看来,宏只是用 C 语言实现 monad 的一种方式。
    【解决方案4】:

    我认为值得在这里添加一个微妙的点。

    monad 设计模式和宏提供了类似的结果,我认为这导致了这个问题。

    Monad 提供了一种提取重复代码的方法,以及在大多数 FP 语言中基于某些条件进行分支的能力,此外还提供了通过用我们可以控制的上下文包装一些底层类型来对两个函数进行排序的替代方法。

    然而,在 lisps 中我们不需要它,因为宏更强大并且提供相同或更多的功能。我们可以随意重新排列代码。因此我们在 Clojure 中使用 threading 宏、cond 宏、let 宏等。

    因此,正如我们所见,合法或非法的 monad 在代码操作支持有限的语言中非常有用,但如果您使用 lispy 宏通常更有用。

    【讨论】:

      猜你喜欢
      • 2019-09-10
      • 1970-01-01
      • 2016-06-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-09-17
      • 2017-10-22
      • 2017-01-17
      相关资源
      最近更新 更多