Reputation: 2078
Let’s say that I have a library xx with namespace xx.core
, and I’m writing it in pure Clojure, intending to target both Clojure and ClojureScript. The de facto way to do this seems to using lein-cljsbuild’s crossovers and conditional comments. So far, so good. This premise is now obsolete. lein-cljsbuild has been deprecated in favor of reader conditionals, and there are many other namespace/macro ClojureScript enhancements. See the updated answer below.
Let’s say xx has a bunch of vars that I want its users, both in Clojure and ClojureScript, to be able to use. These can be split into three sorts of vars.
However, since ClojureScript requires macros to be separated from regular .cljs
namespaces in their own special .clj
namespaces, all macros have to be isolated away from all other vars in xx.core
.
But the implementation of some of those other vars—the type-2 vars—incidentally depend on those macros!
(To be sure, only macros seem to be accessible using ClojureScript’s use-macro
or require-macro
. Just now I tested this; I tried out simply keeping everything (macros, type-1 vars, and type-2 vars) inside a single xx/core.clj
file, and referring to it in a ClojureScript test file using (:use-macro xx.core :only […])
. The compiler then emits a WARNING: Use of undeclared Var
message for each non-macro var in xx.core
that the ClojureScript file referred to.)
What do people tend to do in this situation? The way I see it, the only thing I can do is to…split the library’s public API into three namespaces: one for type-1 vars, one for macros, and one for type-2 vars. Something like…xx.core
, xx.macro
, and xx.util
?…
Of course, this sort of stinks, since now any user of xx (both in Clojure or ClojureScript) has to know whether each var (of which there may be dozens) happens to depend on a macro in its implementation, and which namespace it thus belongs to. This wouldn’t be necessary if I targeted Clojure only. Is this really how it is right now, if I want to target both Clojure and ClojureScript?
Upvotes: 4
Views: 1147
Reputation: 2078
This question’s premise was largely obsoleted several years ago. I’m doing my part to not pollute the web with outdated information, with this update.
ClojureScript still, unlike Clojure, usually compiles macros in a compilation stage separate from the runtime. There is still much incidental compexity. However, the situation has vastly improved thanks to several enhancements.
Since version 1.7 in 2015, Clojure and ClojureScript now support reader conditionals, which enable macros and functions to be defined in the same .cljc
file for Clojure, ClojureScript, Clojure CLR, or all three: #?(:clj …, :cljs …, :cljr …, :default …)
. This alone mitigates much of the problem.
In addition, ClojureScript itself now has had several enhancements to ns
that get rid of much other incidental complexity for users of a namespace. They are now documented in Differences from Clojure, § Namespaces. They include implicit macro loading, inline macro specification, and auto-aliasing clojure
namespaces:
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-existingclojure.*
namespace is required or used and a matching cljs.* namespace exists, thecljs.*
namespace will be loaded and an alias will be automatically established from theclojure.*
namespace to thecljs.*
namespace. For example:(ns testme.core (:require [clojure.test]))
will be automatically converted to
(ns testme.core (:require [cljs.test :as clojure.test]))`
Lastly, ClojureScript now has a second target: bootstrapped, self-hosted ClojureScript, aka CLJS-in-CLJS. In contrast to the CLJS-on-JVM compiler, the bootstrapped ClojureScript compiler actually can compile macros! Separation is still enforced in source files, but its REPL can run them intermingled with functions fine.
Mike Fikes has written a series of valuable articles on these and other issues on Clojure–ClojureScript portability, while these features were being developed. These include:
Even in 2017, it is exciting to watch ClojureScript continue to mature.
Upvotes: 4
Reputation: 91554
It seems you understand the situation properly :)
regarding: "any user of xx (both in Clojure or ClojureScript) has to know whether each var (of which there may be dozens) happens to depend on a macro in its implementation, and which namespace it thus belongs to."
You can add two more namespaces, api.clj and api.cljs which include the proper namspaces for each of the vars for that api and take some of the pain out of that decision. It seems that this area is still quite new though.
Upvotes: 3