双重Clojure/ClojureScript库设计和宏

4
假设我有一个名为xx的库,其命名空间为xx.core,我使用纯Clojure编写它,旨在针对Clojure和ClojureScript。目前来看,似乎使用 lein-cljsbuild的交叉引用和条件注释是事实上的方法。到目前为止,一切都很好。 这个前提现在已经过时了。lein-cljsbuild已被弃用,改用 reader conditionals,还有许多其他的命名空间/宏ClojureScript增强功能。请参见下面的更新答案 假设xx有一堆变量,我希望它的用户(包括Clojure和ClojureScript)能够使用。这些变量可以分为三种类型。
  • 依赖于xx中没有宏的函数/其他变量(我称之为类型1变量)
  • 仅碰巧依赖于xx中的宏的函数/其他变量(我称之为类型2变量)

然而,由于ClojureScript要求将宏与常规的.cljs命名空间分开放置在它们自己的特殊.clj命名空间中,因此所有宏都必须与xx.core中的所有其他变量隔离开来。

但是,其中一些其他变量的实现-类型2变量-恰好依赖于这些宏!

(为了确保,ClojureScript 的 use-macrorequire-macro 似乎只能访问宏。我刚刚测试了一下;我尝试将所有内容(宏、类型1变量和类型2变量)都放在一个单独的 xx/core.clj 文件中,并在 ClojureScript 测试文件中使用 (:use-macro xx.core :only […]) 引用它。然后编译器会针对 ClojureScript 文件引用的 xx.core 中的每个非宏变量发出一个 WARNING: Use of undeclared Var 消息。)
人们在这种情况下通常会怎么做?在我看来,我唯一能做的就是……将库的公共 API 分成三个命名空间:一个用于类型1变量,一个用于宏,一个用于类型2变量。类似于……xx.corexx.macroxx.util?……
当然,这有点糟糕,因为现在任何使用xx的用户(无论是Clojure还是ClojureScript)都必须“知道”每个变量(可能有数十个)在其实现中是否依赖于宏,以及它所属的命名空间。如果我只针对Clojure,则不需要这样做。如果我想同时针对Clojure和ClojureScript,现在真的是这样吗?
2个回答

4

这个问题的前提在几年前就已经过时了。通过这个更新,我正在尽我的一份力量,避免用过时的信息污染网络。

ClojureScript仍然与Clojure不同,通常会在编译阶段而非运行时阶段编译宏。仍然存在许多偶发性的复杂性。但是,由于几项增强措施,情况已经得到了极大改善。

自从2015年的版本1.7以来,Clojure和ClojureScript现在支持读取条件语句,这使得宏和函数可以在同一个.cljc文件中为Clojure、ClojureScript、Clojure CLR或所有三者定义:#?(:clj …, :cljs …, :cljr …, :default …)。仅这一点就大大缓解了问题。

此外,ClojureScript本身现在已经对ns进行了多项增强,为命名空间的用户消除了很多其他的附加复杂性。它们现在记录在与Clojure的区别, § 命名空间中。它们包括隐式宏加载、内联宏规范和自动别名clojure命名空间。

Implicit macro loading: If a namespace is required or used, and that namespace itself requires or uses macros from its own namespace, then the macros will be implicitly required or used using the same specifications. Furthermore, in this case, macro vars may be included in a :refer or :only spec. This oftentimes leads to simplified library usage, such that the consuming namespace need not be concerned about explicitly distinguishing between whether certain vars are functions or macros. For example:

(ns testme.core (:require [cljs.test :as test :refer [test-var deftest]])) will result in test/is resolving properly, along with the test-var function and the deftest macro being available unqualified.

Inline macro specification: As a convenience, :require can be given either :include-macros true or :refer-macros [syms…​]. Both desugar into forms which explicitly load the matching Clojure file containing macros. (This works independently of whether the namespace being required internally requires or uses its own macros.) For example:

(ns testme.core
  (:require [foo.core :as foo :refer [foo-fn] :include-macros true]
            [woz.core :as woz :refer [woz-fn] :refer-macros [apple jax]]))

is sugar for

(ns testme.core
  (:require [foo.core :as foo :refer [foo-fn]]
            [woz.core :as woz :refer [woz-fn]])
  (:require-macros [foo.core :as foo]
                   [woz.core :as woz :refer [apple jax]]))

Auto-aliasing clojure namespaces: If a non-existing clojure.* namespace is required or used and a matching cljs.* namespace exists, the cljs.* namespace will be loaded and an alias will be automatically established from the clojure.* namespace to the cljs.* namespace. For example:

(ns testme.core (:require [clojure.test]))

will be automatically converted to

(ns testme.core (:require [cljs.test :as clojure.test]))`
最后,ClojureScript 现在有了第二个目标:自举、自托管的 ClojureScript,也称为 CLJS-in-CLJS。与 CLJS-on-JVM 编译器相比,自举 ClojureScript 编译器实际上可以编译宏!尽管在源文件中仍然执行分离,但它的 REPL 可以很好地混合运行它们和函数。
Mike Fikes 在这些功能被开发期间撰写了一系列有价值的文章,涉及 Clojure-ClojureScript 可移植性等问题。其中包括: 即使到了2017年,观察ClojureScript继续成熟仍然令人兴奋。

3

看起来你已经很好地理解了这种情况 :)

关于:"任何使用xx的用户(包括Clojure或ClojureScript)都必须知道每个变量(可能有数十个)是否依赖于宏以及它属于哪个命名空间。"

你可以添加两个命名空间api.clj和api.cljs,其中包含该API中每个变量的正确命名空间,并减轻一些决策痛苦。不过,这个领域似乎仍然很新。


谢谢你的回答!很遗憾事情是这样的...当你说“您可以添加两个更多的命名空间,api.clj和api.cljs,它们包括每个API变量的适当命名空间”时,您能否详细说明一下?是否有一种方法可以从一堆命名空间中导出变量以将它们组合成单个命名空间?(我想另一个选择是尝试重写每个非宏变量,以不使用任何我的宏...呃。太多的附带复杂性。) - jschoi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接