(ns foo.core)
(defmacro add
[a b]
`(+ ~a ~b))
ClojureScript 的 ns
表达式在大多数情况下都非常简单,与 Clojure 类似,尤其是在最近添加了 ClojureScript 的功能之后。
但是,ns
表达式背后有一套丰富的选项,您可能会在实际使用中遇到它们。本指南旨在介绍这些选项并提供一些清晰的解释。
在 ClojureScript 中,宏的处理方式与 Clojure 略有不同。特别是,ns
表达式支持 :require-macros
,以及我们将在此处介绍的一些简化语法糖和隐式加载行为。
为了举例说明,假设我们在 src/foo/core.clj
中有以下内容
(ns foo.core)
(defmacro add
[a b]
`(+ ~a ~b))
要在一些 ClojureScript 源代码中使用此宏,您可以使用 :require-macros
规范,如下所示
(ns bar.core
(:require-macros [foo.core]))
(foo.core/add 2 3)
现在,:require-macros
的设计与 :require
非常相似。因此,例如,您可以直接将 add
符号引用到您的命名空间中
(ns bar.core
(:require-macros [foo.core :refer [add]]))
(add 2 3)
上面方法的另一种替代方法(很少见,但为了完整性值得介绍)是
:use-macros
。在 ClojureScript 中,:require
/:refer
和:use
/:only
本质上是彼此的双重形式。因此,上面的ns
表达式也可以写成(ns bar.core (:use-macros [foo.core :only [add]]))
。
您还可以设置命名空间别名 foo
(ns bar.core
(:require-macros [foo.core :as foo]))
(foo/add 2 3)
上面的示例都在使用 :require-macros
基元。
但是,您经常会使用来自库的代码,这些库同时提供运行时代码(函数和其他 def
s)和宏,所有这些都来自同一个命名空间。
因此,继续我们的示例,假设有一个 src/foo/core.cljs
文件,其中包含
(ns foo.core)
(defn subtract
[a b]
(- a b))
现在,如果您想使用 add 和 subtract,您可能会这样做
(ns bar.core
(:require-macros [foo.core :refer [add]])
(:require [foo.core :refer [subtract]]))
(add 2 3)
(subtract 7 4)
但是,有一个 ns
表达式语法糖,:refer-macros
,可以让您改写成这样
(ns bar.core
(:require [foo.core :refer [subtract] :refer-macros [add]]))
(add 2 3)
(subtract 7 4)
上面的 :refer-macros
实际上只是语法糖,它会被编译器算法地反糖成前面的形式。
同样,还有 :include-macros
语法糖,您可以使用它来指示宏命名空间应该使用与运行时命名空间相同的规范进行加载。例如,这可以工作
(ns bar.core
(:require [foo.core :as foo :include-macros true]))
(foo/add 2 3)
(foo/subtract 7 4)
上面的代码会反糖成更冗长的基元形式
(ns bar.core
(:require-macros [foo.core :as foo])
(:require [foo.core :as foo]))
(foo/add 2 3)
(foo/subtract 7 4)
在运行时命名空间内部需要加载其自身宏命名空间(指具有相同名称的命名空间)的情况下,您会自动获得 :include-macros
语法糖。如果您正在开发库,建议这样做,用户可以简化他们的加载形式。
为了说明这一点,假设我们的 src/foo/core.cljs
文件改成这样
(ns foo.core
(:require-macros foo.core))
(defn subtract
[a b]
(- a b))
现在,您可以像这样使用代码
(ns bar.core
(:require [foo.core :as foo]))
(foo/add 2 3)
(foo/subtract 7 4)
那么这个简化形式如何呢?
(ns bar.core
(:require [foo.core :refer [add subtract]))
(add 2 3)
(subtract 7 4)
在这种情况下,add
是宏,subtract
是函数,编译器会自动处理这些差异,因此可以使用统一的引用变量,ns
表达式看起来与 Clojure 中的几乎一样。
如果您在 REPL 中需要快速参考上述主题,ns
特殊形式的文档字符串可以提供帮助。语法糖形式被称为内联宏规范,隐式语法糖被称为隐式宏加载。文档字符串中包含一个相当全面的反糖示例。在紧急情况下,(doc ns)
是您的好帮手。
require
和 require-macros
宏您可以使用 require
和 require-macros
将代码动态加载到您的 REPL 中。有趣的是,上面描述的功能也适用于这些宏。
这是一个实现细节,但它可以帮助您了解它是如何实现的:当您在 REPL 中发出
(require-macros '[foo.core :as foo :refer [add]])
时,它会在内部被转换为类似于以下的 ns
表达式
(ns cljs.user
(:require-macros [foo.core :as foo :refer [add]]))
重要的是,当您使用 require
时,会使用类似的 ns
表达式,并且它会受到上面描述的所有反糖和推断行为的影响。
clojure
命名空间别名一些命名空间,例如 clojure.string
和 clojure.set
,可以在 ClojureScript 中使用,即使这些命名空间的第一部分是 clojure
。但是,另一些,例如 cljs.pprint
、cljs.test
和现在的 cljs.spec
,位于 cljs
下面。
为什么会有区别?理想情况下,应该没有区别。但是,如果您查看,例如,clojure.pprint
的 ClojureScript 移植版本,它涉及宏命名空间。这就是问题所在。由于 JVM ClojureScript 编译器使用 Clojure 进行执行,如果端口没有移到 cljs.pprint
,就会出现命名空间冲突。简而言之,clojure.pprint
命名空间已被占用。
这样做的结果是,在编写 ClojureScript 时,我们必须记住对一些命名空间使用 cljs.*
。并且,如果您正在编写可移植代码,则需要使用读取器条件。
ns
表达式有一个相对较新的简化方法,您可以使用它:在不存在的 clojure.*
命名空间(可以映射到 cljs.*
命名空间)的情况下,您可以使用 clojure
来代替命名空间的第一部分中的 cljs
。
一个简单的例子
(ns foo.core
(:require [clojure.test]))
可以用来代替
(ns foo.core
(:require [cljs.test]))
如果您这样做,ClojureScript 编译器会首先查看它是否可以加载 clojure.test
命名空间。由于它不存在,它将回退到加载 cljs.test
。
同时,还会从 clojure.test
到 cljs.test
设置一个别名,就像您写了以下代码一样
(ns foo.core
(:require [cljs.test :as clojure.test]))
这很重要,因为它允许您具有限定符号的代码,如 clojure.test/test-var
。
通过使用此别名,以及在 :refer
规范中推断宏变量的能力(参见上面的“隐式引用”),以下代码在 ClojureScript 中可以正常工作
(ns foo.core-test
(:require [clojure.test :as test :refer [deftest is]]))
(deftest foo-test
(is (= 3 4)))
(test/test-var #'foo-test)
更重要的是:这与您在 Clojure 中编写的代码完全相同。无需使用读取器条件!
当然,这在 require
ClojureScript 宏中也适用。因此,例如,您可以执行
(require '[clojure.spec :as s])
然后 (s/def ::even? (s/and number? even?))
将可以正常工作。原因是 require
宏是用 ns
特殊表达式实现的。
希望这些详细的示例有助于阐明 ns
反糖、推断和别名是如何工作的。总的来说,目的是简化 ClojureScript ns
表达式的使用,但是了解这些额外功能是如何工作的,可以更好地理解那些您需要知道或想了解实际情况的时候。
充分利用这些功能可以帮助您更好地消除 ClojureScript 和 Clojure ns
表达式之间的差异。
原作者:Mike Fikes