【问题标题】:How to identify which forms are macros and which are functions while looking at a Clojure code?如何在查看 Clojure 代码时识别哪些表单是宏以及哪些是函数?
【发布时间】:2013-07-24 06:01:40
【问题描述】:

Lisp/Clojure 代码在语法上具有一致性,这是一个优点,因为不需要理解各种不同的结构。 但有时通过使用不同的语法来查看一段代码更容易理解,例如 this is a switch casethis is the pattern matching structure 等没有实际阅读文本。

几个月前我开始使用 Clojure,我意识到如果不阅读表单的名称,然后在谷歌上搜索它是宏还是函数以及它是如何工作的,我将无法理解代码。

事实证明,一段 Clojure 代码,无论语法的统一性如何,都是不统一的。

它可能看起来像一个函数,但如果它是一个宏,那么它可能不会评估它的所有参数。

是否存在所有宏都使用的命名约定或缩进样式,以便人们通过名称更容易掌握发生了什么?

【问题讨论】:

  • 好问题。我认为没有办法仅通过观察来了解差异。作为一个初学者,你被告知语法很简单,一切都是 (fn args),那么你问为什么不 (apply or coll) ?或者为什么不(地图报价科尔)?哦,它们是宏(或特殊形式)。你只是学会猜测哪个是哪个。
  • hehe: (fn?+) true (fn?->) 不能取宏的值!而且,没有宏?还是特殊形式?但是,(:macro (meta #'->)) true
  • @Hendekagon:这是一个人从经验中学到的东西,但人们学习的不仅仅是猜测,而是做出有根据的猜测(通常是(接近)确定性)。以(map quote coll) 为例,由于语言的基本属性,quote 不可能是函数,并且无论如何(map quote coll) 是接收coll 后评估的函数调用,所以不清楚是什么是有意的(尽管(map #(list 'quote %) ...) 在宏定义中可能非常有用,只要... 是宏可以访问的集合)。
  • @michal-marczyk 但原则上没有什么可以阻止引用作为函数,它只是不在 Clojure 中(例如,它是内核中的函数(或“操作”))
  • @Hendekagon:你也可以说没有什么可以阻止if 成为一个函数,因为你可以在 Haskell 中编写这样的函数。当然,很明显,在没有像 Clojure 这样普遍的惰性求值的语言中,这是不可能的,因此理解“语言的基本属性”(引用我上面的评论)会导致正确的期望。同样,与 Kernel 相比,Clojure 没有用于诱导评估的显式构造,因此无法具有有意义的 quote 函数。

标签: clojure clojurescript


【解决方案1】:

在我看来,最有用的直觉来自于理解给定运算符 / Var 的目的。设计良好的宏根本不能写成函数,并且仍然提供具有相同语法的相同功能,因为如果可以,它们实际上会被写成函数(参见上面的“精心设计”部分!)。1 所以,如果你正在处理一个不可能是常规函数的构造,那么你就知道它不是;否则很可能是。

此外,了解库导出的 Var 的常用方法会告诉您预先处理的是宏还是函数。 doc 也是如此((doc foo) 表示 foo 是靠近其输出顶部的宏,如果确实如此),source(因为它为您提供了整个代码)和 M -.(使用 nrepl.el 或 swank-clojure 跳转到 Emacs 中的定义;M-, 跳转回来)。文档可能会提到什么是宏,什么不是(除非文档字符串不一定如此,因为访问文档字符串的所有常用方法都已经告诉您是否正在处理宏,如上所述)。

如果您浏览一段代码的目的是粗略了解它可能会做什么,假设各种运算符执行其名称所暗示的功能,那么 (1) 名称具有足够的暗示性并且您对代码的意图有所了解,因此您甚至不需要关心哪些运算符恰好是宏,或者 (2) 名称没有足够的暗示性,因此您需要深入研究文档或无论如何,某些运算符的来源,然后您将了解的第一件事是它们中的哪些被注册为宏。

最后,宏没有单一的命名风格,尽管有一些特定于特定用例的约定。例如with-foo 风格的结构往往是便利宏,其目的是简化处理foo 类型的资源; dofoo 风格的结构往往是宏,它需要执行一组表达式(多少次以及设置的附加上下文取决于宏;这个家族中最基本的成员,do,实际上是一个特殊形式而不是宏); deffoo-style 构造引入了新的 Vars 或类似类型的实体。

值得指出的是,类似的模式有时会被打破。例如,大多数线程结构(-> & Co.)都是宏,但来自clojure.data.zip.xmlxml-> 是一个函数。当考虑到所提供的功能时,这是完全有道理的,这让我们回到了运营商作为最有用的直觉来源的目的。


1 此规则可能有一些例外。人们会期望这些被记录在案。当然,有些项目根本没有记录(或几乎没有记录);到这里,问题就完全消失了,因为无论如何都必须找到源头才能理解事物。

【讨论】:

    【解决方案2】:

    通常有两个属性可以将宏(或有时是特殊形式)与函数区分开来:

    1. 当表单进行某种类型的绑定(即声明新的标识符供以后使用)时
    2. 当某些参数被延迟评估时

    第一种情况的示例是letletfnbindingwith-local-vars。奇怪的是,defn 被定义为一个函数,但我很确定它与 Clojure 的引导过程有关(defn 是在定义 defmacro 之前定义的)。

    第二个例子是andorlazy-seq。在所有这些构造中,通过将参数放入条件分支(如if)或将它们移动到函数体中来懒惰地评估参数。

    这两个属性实际上只是宏操纵 Clojure 语法的表现。我不认为线程宏(->->>)非常适合这两个类别,但是零安全版本​​(-?>-?>>)有点属于 lazy论据

    【讨论】:

    • defn 是一个宏;它在其定义之后立即由(. (var defn) (setMacro)) 注册。正如您所指出的那样,其原因是当时defmacro 不可用。事实上,defmacro 包装了这个模式(defn 后跟 setMacro 调用)并进行了一些额外的预处理(以注入隐式 &form&env 参数)。
    • @MichałMarczyk - 感谢您确认。 defn 的定义似乎返回(引用)语法,因此它看起来像一个宏,即使它是用 deffn 定义的。我想这是有道理的,因为宏只是在编译时而不是运行时调用的函数。也感谢您指出setMacro 部分!
    【解决方案3】:

    据我所知,没有强制的命名约定。

    根据经验,尽可能首选函数,但是当宏遵循def<something> 设置某事或with-<resource> 使用开放资源执行某事的模式时,有时会发现宏。

    因此,您可能会发现 clojure 的 doc 宏很有帮助。它会告诉你表单是否是宏/函数/特殊表单,并给出它的 arg 列表和 doc 字符串(如果存在)。例如

    (use 'clojure.repl)
    (doc and)
    

    将以下内容打印到 repl。

    clojure.core/和

    ([] [x] [x & 下一个])

    从左到右一次计算一个表达式。如果一个表格 返回逻辑假(nil 或假),并返回该值和 不计算任何其他表达式,否则返回 最后一个 expr 的值。 (and) 返回 true。

    一些编辑器(例如 emacs)会以弹出窗口或组合键的形式提供此文档,这样可以更快地访问(和阅读)它。

    【讨论】:

    • "因此,您可能会发现 clojure 的 doc 宏很有帮助。它会告诉您表单是否是宏/函数/特殊表单,并给出它的 arg 列表和 doc 字符串(如果存在)" 这绝对是有帮助的。这适用于开发人员自己定义的宏?
    • 是的。编译器知道它应该将函数用作宏,因为它存储的 Var 在其元数据映射中包含 :doc truedoc 查找相同的元数据。
    猜你喜欢
    • 1970-01-01
    • 2015-12-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-22
    • 2017-09-13
    • 1970-01-01
    • 2013-05-14
    相关资源
    最近更新 更多