【问题标题】:Clojure Macros: When can a function not duplicate a macro's behaviour?Clojure 宏:函数何时不能复制宏的行为?
【发布时间】:2018-11-19 15:44:25
【问题描述】:

我正在使用 clojure 宏,我发现我可以通过函数组合来复制很多宏行为。

线程宏就是一个很好的例子:

(defn add1 [n] (+ n 1))
(defn mult10 [n] (* n 10))

(defn threadline [arg]
  (-> arg
      add1
      mult10))

我可以用像管道这样的高阶函数轻松地复制它:

(defn pipe [& fns]
  (reduce (fn [f g] (fn [arg] (g(f arg)))) fns))

(def pipeline
  (pipe
   #(+ % 1)
   #(* % 10)))

必须存在不能用函数替换宏的情况。我想知道是否有人有这类情况的一些很好的例子,以及所涉及的反复出现的主题。

【问题讨论】:

  • 理论上,任何人都可以做其他人的工作,无论是图灵完备还是全部。但是,您是否愿意是另一回事。
  • @dsm 不正确。宏不必评估所有参数,但函数可以。此外,宏不能传递给高阶函数,例如map,但函数可以。
  • 不,他们没有。你有懒惰的 eval 和 lanbda
  • *lambda - 傻胖的粗短手指...

标签: clojure macros


【解决方案1】:

宏的一个重要优势是它们能够在编译时转换代码而无需评估任何代码。宏在编译期间接收代码作为数据,但函数在运行时接收。宏允许您在某种意义上扩展编译器。

例如,Clojure 的 andor 被实现为递归宏,可扩展为嵌套的 if 形式。这允许对and/or 的内部形式进行惰性求值,即如果第一个or 形式为真,则将返回其值,并且不会评估其他任何形式。如果您将and/or 编写为一个函数,则它的所有参数都将在被检查之前被评估。

在您的 pipe 函数示例中,短路控制流不是问题,但与 -> 相比,pipe 增加了相当大的运行时复杂性,-> 只是展开为嵌套形式。尝试将其作为函数实现的更有趣的宏可能是 some->

我发现很多宏行为我可以通过函数组合来复制

如果你的函数适合它,你当然可以用 comp 替换带有函数组合的简单线程宏,类似于其他函数语言中的“无点”风格:#(-> % inc str) 在功能上等同于 (comp str inc)#(str (inc %)).

通常建议尽可能选择函数,即使在编写宏时,您通常也可以将大部分“工作”交给函数。

【讨论】:

    【解决方案2】:

    我学过的第一个宏是一个很好的例子,说明不能写成普通函数的宏:

    (defmacro infix [[arg1 f arg2]]
      (list f arg1 arg2))
    
    (infix (1 + 2))
    => 3
    

    当然,这个确切的宏永远不会在野外使用,但它为充当可读性帮助器的更有用的宏奠定了基础。还应该注意的是,虽然您可以使用普通函数复制许多基本宏的行为,应该吗?很难说你的管道示例比as-> 更容易读/写代码。

    您正在寻找的“重复出现的主题”是在编译时(“数据”是代码本身)而不是运行时处理数据的情况。任何需要函数对其参数不求值的情况都必须是宏。在某些情况下,您可以部分“作弊”,只需将代码包装在一个函数中以延迟评估,但这并不适用于所有情况(如infix 示例)。

    【讨论】:

      【解决方案3】:

      宏不能与函数互换,但你的例子是:

      (macroexpand '#(+ % 1))
      ; ==> (fn* [p1__1#] (+ p1__1# 1))
      

      它起作用的原因是因为参数期望一个函数,而你使用了一个变成函数的宏。但是我知道cond 是一个宏。它不能被函数实现替换,因为函数的参数被评估并且cond 的全部要点是仅根据对其谓词的评估以特定顺序评估参数的某些部分。例如。使用 that 进行递归函数将永远不会终止,因为在评估函数 cond 的主体之前也将始终调用默认情况。

      宏的全部意义在于扩展语言,并且由于评估由结果控制,因此您可以创建各种新功能,这些功能在函数中是不可能的,除非将所有参数作为函数传递以延迟评估。

      【讨论】:

      • 这很清楚。我也试图弄清楚这一点,但发现大多数反应真的很混乱。对于 Clojure 的外行来说,这条线似乎太模糊了,无法区分它们。您能否提供您在答案末尾提到的这些示例中的一两个?感谢您分享此回复!
      【解决方案4】:

      在任何语言中,宏——从代码到代码的编译时函数——让你做三件事:

      1. 定义新的绑定形式(例如 Clojure 的解构 let)。
      2. 更改评估顺序(例如orand)。
      3. 提供特定领域的语言(例如 Instaparse)。

      您可以争论 3 —— 实现 DSL 是否真的需要宏。当然,您可以生成从文本文件到文本文件的函数的代码生成器。或者,确定你可以做 Ruby 风格的运行时 DSL。但是,如果您想要在编译时将 DSL 集成到编译器中,那么宏实际上就是您的“编译器 API”。

      话虽如此,仅将宏用于这些特殊目的是有意义的。尽可能使用函数和/或数据驱动的代码。包括在宏提供的“外观”后面做工作。

      【讨论】:

        【解决方案5】:

        宏做的两件大事是控制对其参数的评估和在编译时转换代码。您可以通过要求调用代码引用它们的参数来对函数进行这两种操作。

        例如,您可以编写一个这样调用的defn 版本:

        (defn 'name '[arg1 arg2]
          '(expression1)
          '(expression2)
          'etc)
        

        然后您可以随意eval 参数,评估或不评估它们,更改执行顺序或在评估它们之前更改形式,这正是宏的好处。

        宏可以做而函数做不到的事情是在没有调用代码的任何合作的情况下获得这种能力。用户可以像调用普通函数一样调用宏,而不必区别对待它们的参数。

        这就是宏允许您扩展语言的方式:您不必以与常规代码不同的方式处理宏代码,这与 JavaScript、Ruby 或 Python 不同,其中语言只能通过新的控制流进行扩展通过执行您在示例中所做的操作,将代码包装在块、lambda 或函数中来构建。

        【讨论】:

        • 您实际上不能以这种方式编写defn,至少如果您想要它的所有当前功能则不能。重要的区别在于 eval 无法访问词法范围,因此像 (let [x 1] (defn* 'foo '[] 'x)) 这样的东西不能工作,而 (let [x 1] (defn foo [] x)) 工作正常。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-07-28
        • 2011-12-12
        • 2011-08-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多