anmonteiro Code ramblings

ClojureScript `require` outside `ns`

The next version of the ClojureScript compiler adds support for using require outside of the ns form. Owing to ClojureScript’s compilation model, however, there exist subtle differences with respect to the behavior that Clojure provides. Read on to learn more.

Update: This has been released as part of ClojureScript version 1.9.293.

Background

Ever since its inception, ClojureScript has always required files to provide a namespace declaration at the top. A hard requirement like that is sometimes counterproductive when we just want to try something out quickly. Besides, it doesn’t mimic Clojure’s behavior very well, where it assumes you’re in the default user namespace when a namespace declaration is not provided. In addition, forms like require, use and import were never supported outside of the ClojureScript REPL, and even those were implemented as a hack on top of ns.

A number of tickets (1, 2) had been lying around in ClojureScript’s issue tracker to address these shortcomings. Now, a recent addition to the compiler (coming in the next release) fixes these issues, paving the way for a number of enhancements coming to ClojureScript in the near future.

Differences from Clojure

In Clojure, you can require namespaces dynamically into your namespace, load code conditionally and at arbitrary nesting levels. In ClojureScript, this is not the case, mostly due to the static nature of namespaces in the Google Closure Library. Consequently, there are couple of rules to abide by when calling require and similar forms (use, use-macros, etc.) in ClojureScript:

  1. A file can have one of:
    • a namespace declaration, or
    • (possibly several) require forms, or
    • none of the above, in the case it doesn’t depend on any other namespace.
  2. when present, require forms must appear at the top of a file.
  3. All the definitions in a file without a namespace declaration are interned in the cljs.user namespace 1.

Example

Because this new feature is currently unreleased, the only way you can try it out today is by building the compiler uberjar (with script/uberjar). Below is an example you can use to guide you through this new addition.

Create a project with the following structure:

project
├─ src
│  └─ foo.cljs
└─ cljs.jar

In foo.cljs, place the contents below. As an aside, note that we’re taking advantage of Clojure namespace aliasing (JIRA ticket) and implicit macro loading. This too — as one might expect — works outside the namespace declaration.

;; src/foo.cljs
(require '[clojure.test :refer [deftest is run-tests]])

(deftest failing-test
  (is false))

(run-tests)

With the cljs.jar uberjar in place, running the following command will land you directly in a ClojureScript Node.js REPL (note: requires a Node.js installation).

$ java -cp src:cljs.jar clojure.main -e "(require 'cljs.repl) (require 'cljs.repl.node) (cljs.repl/repl (cljs.repl.node/repl-env))"

Then you can load the file into the REPL and see it execute!

ClojureScript Node.js REPL server listening on 53518
To quit, type: :cljs/quit

cljs.user=> (load-file "src/foo.cljs")

Testing cljs.user

FAIL in (failing-test) (at .cljs_node_repl/cljs/test.js:432:14)
expected: false
  actual: false

Ran 1 tests containing 1 assertions.
1 failures, 0 errors.
nil
cljs.user=>

I’ve also captured all the above in a gist that you can refer back to later.

Parting thoughts

This addition unlocked a number of new possibilities for ClojureScript, which are already bearing fruits. First of all, it already allowed us to delete a ton of hacky code that made require work in REPLs. In a very near future, it will allow ClojureScript to add a number of new exciting features, one of which is support for extensible tagged literals.

Thanks for reading!


1 If a ns form is not present, a generated namespace will be provided in the form of cljs.user.file_nameXXXX, where XXXX are the first characters of the filenames's SHA1 hash. This implementation detail effectively works around the fact that the Google Closure Library requires files to provide different namespaces. ↩


Thanks to Mike Fikes for reading a draft of this post.