Chapter 7. Namespaces, Libraries, and Google Closure

So far, we’ve talked mostly about basic features of the language such as syntax, semantics, and the compilation process. ClojureScript also offers compelling features at a higher structural level to facilitate code organization and sharing libraries.

In ClojureScript, as in Clojure, the highest level of code organization is namespaces, used to scope global definitions. However, despite superficial similarities, namespaces in ClojureScript are implemented completely differently than they are in Clojure because it runs in a different environment. This chapter will cover what these differences are and how to use namespaces effectively in ClojureScript.

Additionally, this chapter will describe how to create and utilize libraries. Unfortunately, one of the negative effects of ClojureScript’s reliance on the Google Closure Compiler is the fact that creating and consuming libraries is not always straightforward, particularly in light of Google Closure’s Advanced Optimizations mode.

Namespaces

To avoid name collisions, ClojureScript symbols and keywords have a namespace component. Each *.cljs file has its own namespace, and every REPL session has a current namespace (cljs.user by default). Whenever you define a symbol using def or one of its derivatives (such as defn), the namespace of the symbol is set to the current namespace. Symbols with the same name in different namespaces are completely different, and will not clash.

In addition to name disambiguation, namespaces are also ClojureScript’s unit of code dependency management. A namespace may require or use other namespaces as dependencies, and dependent namespaces are always loaded before the namespace that required them when the program is run. ClojureScript does not support circular references between namespaces.

One very important difference between namespaces in ClojureScript and Clojure is that while Clojure namespaces are first-class entities that can be dynamically created and loaded at runtime, ClojureScript namespaces are statically resolved at compile time only. You can’t create or load a namespace during program execution.

Using Namespaces

As you have already seen, if you refer to a symbol without specifying a namespace, ClojureScript will attempt to resolve the symbol in the current namespace. To specify a namespace, use a slash between the namespace and the name. For example, foo.bar/hello references a symbol named hello in the foo.bar namespace, which is distinct from a symbol named hello in any other namespace.

To specify a namespace for a source file, use the ns special form as the first form in the file. (Unlike Clojure, ns is not just a macro wrapping more primitive functions (such as load and require) but built in to the language directly.) In its most basic form, declaring a namespace called foo.bar looks like this:

(ns foo.bar)

In ClojureScript, as in Clojure, the namespace of a file needs to match its location on the Java classpath. In the case of ClojureScript, this is the classpath of the compiler. Each . (period or full stop) in the namespace translates to a subdirectory on the classpath. For example, the namespace foo.bar.baz should be in a file with the path foo/bar/baz.cljs, relative to the classpath. This is necessary for the ClojureScript compiler to find the file that corresponds to a namespace.

To specify that a namespace depends on another namespace, add a :require form in the ns form, containing any number of specifications:

(ns application
    (:require [foo.bar :as bar]
              [foo.baz :as baz]))

This will force loading of the foo.bar and foo.baz namespaces before application itself is loaded. Labeling the namespaces with the :as clause means that within the foo namespace, you can reference the symbols in foo.bar via a shorter “alias” bar. In this example, the symbol foo.bar/function can be written bar/function. ClojureScript, unlike Clojure, does not permit “bare” namespaces in a :require clause. The compiler will throw an exception if you specify a :require without :as.

You may also reference namespaces with :use instead of :require, like so:

(ns application
    (:use [foo.bar :only [hello goodbye]]))

The :use form also causes the specified namespace(s) to be loaded, but differs from :require in that, for each of the symbols specified in the :only vector, it establishes a synonym in the current namespace so you can refer to symbols in the used namespace without explicitly qualifying them. For example, given the above ns declaration, you can now use the symbols hello and goodbye directly in the foo namespace to refer to foo.bar/hello and foo.bar/goodbye.

Again, the use of :only in a :use specification is mandatory in ClojureScript, and will cause a compile error if missing. Additionally, unlike Clojure, :require and :use are the only forms allowed within an ns declaration. Clojure’s :import, :refer, etc., are not supported. Therefore, although a valid ClojureScript ns declaration is also always a valid Clojure ns declaration, the inverse is not true: ClojureScript has much narrower requirements for validity.

Using namespaces at the REPL

The ns form will not work in the ClojureScript REPL (either the Rhino REPL or the browser REPL). To switch the current namespace in the REPL, you can use the in-ns special form, passing it the quoted namespace:

ClojureScript:cljs.user> (in-ns 'foo.bar)

This switches the REPL’s current namespace to be foo.bar.

Note that in-ns is a special tool for REPL development, and doesn’t actually exist in ClojureScript’s standard library. It is implemented as a special case in the REPL’s reader. As such, it won’t work at all in *.cljs source files, or anywhere else except in a REPL session.

Also, because ClojureScript namespaces can be defined only at compile time, using in-ns to switch to a namespace does not implicitly create the namespace (as it does in Clojure). If you specify a namespace that isn’t already loaded, the REPL will switch to it, but almost everything you try to do will fail with a “namespace is not defined” error.

Using Namespaces Effectively

Fundamentally, namespaces are just a tool to prevent name clashes, but they also help structure your code into logical units. They function a bit like modules or packages in other languages. Here are a few hints to use namespaces effectively:

  1. Group similar or related functions together in the same namespace.

  2. Try to minimize the number of dependencies (:require and :use expressions) in each namespace, except for one main namespace that ties together everything in your application.

  3. Never have circular dependencies between two namespaces (namespace A depends on B, which depends on A).

The Implementation of Namespaces

To really understand how ClojureScript namespaces work, it’s helpful to know something about their implementation.

Namespaces and *.js files

When you compile a directory containing *.cljs files, the compiler emits a directory with the same structure containing compiled *.js files; each input *.cljs file has exactly one output *.js file with the same name and path.

Each emitted *.js file contains code allowing it to participate in the namespace system provided by the Google Closure Library. You can read about the Google Closure Library’s dependency management system here. Note that as a user of ClojureScript, you don’t have to worry about writing a deps.js file, or using the ClojureBuilder or DepsWriter scripts described in the Google Closure documentation. The ClojureScript compiler performs those functions internally. This is the mechanism by which ClojureScript actually resolves namespace dependencies: under the hood, ClojureScript’s namespace dependency system is that of Google Closure.

In the Google Closure Library, dependency management is handled by two functions: goog.provide() and goog.require(). The goog.provide() function is intended to be called once per file, and passes the namespace the file contains. The goog.require() function may be called multiple times, each time with a dependency of the file. Both are emitted directly by ClojureScript when compiling an ns form. The mapping is very straightforward:

(ns application
    (:require [foo.bar :as bar]
              [foo.baz :as baz]))

It is compiled to:

goog.provide("application");
goog.require("foo.bar");
goog.require("foo.baz");

What happens next depends on what :optimizations mode the ClojureScript compiler is running in.

When using :optimizations :none, the ClojureScript compiler will write a list of calls to goog.addDependency() to the specified :output-to file. These serve to create mappings between namespace names, dependencies, and relative paths, and are necessary to inform Google Closure of the location of dependencies (unlike ClojureScript, Google Closure has no convention regarding source file location). After you include this dependencies file in your web page, calls to goog.require() will dynamically add new <script> tags to the page using the provided relative path.

It follows, then, that when using :optimizations :none you must also make the compiler output directory (specified with :output-dir) publicly available on a relative path appropriate to the URL of the page. This is necessary because the page will end up loading each of the needed *.js files directly. You must also manually include goog/base.js first in your HTML, to bootstrap the Google Closure Library, as we showed in Chapter 3.

On the other hand, when using any :optimizations mode other than :none, the compiler will concatenate all the required files into one gigantic JavaScript file, in dependency order, and write it out as the :output-to file. This is the only *.js file you need to include on your HTML page, since it contains a full copy of every dependency, even the core Google Closure library. Therefore, in this case, it isn’t necessary to expose the output directory like it is with :optimizations :none.

Namespaces and variable names

Because vanilla JavaScript has no built-in namespace support, it’s standard practice to use objects as a poor man’s namespace system, nesting all the variables a library uses under a single top-level object.

The ClojureScript compiler does exactly the same thing, when compiling ClojureScript symbols to JavaScript variables. Each dot-separated level of the namespace becomes a nested object, with the symbol’s name as the final name. For example, the symbol my.cool.new-project/some-data in ClojureScript will be my.cool.new_project.some_data in JavaScript. (Hyphens in ClojureScript symbols become underscores in JavaScript.)

For pure ClojureScript applications, this is just an implementation detail. However, it does become important when you want to call a ClojureScript function from JavaScript, since you must refer to it by its fully qualified (i.e., nested) name.

Advanced Compilation Mode

For the subsequent sections on consuming and producing libraries, one of the major challenges is working with the implications of the Google Closure Compiler’s Advanced Optimizations mode. Before moving on, it will help to have a complete idea of what Advanced mode actually does.

Advanced mode’s blessing, and its curse, is that it deeply and radically transforms your program. It does so in very beneficial ways, decreasing both code size and execution time, sometimes drastically. Advanced mode can and does:

  • Rename variables and functions to shorter names (sometimes called munging)

  • Flatten object nesting

  • Eliminate unused code

  • Create inline functions

  • Optimize performance based on known characteristics of JavaScript runtimes

Essentially, there is nothing that advanced mode might not do to your code, while maintaining the same semantics. Consider the following example:

function print_sum(sum) {
    alert('The sum is ' + sum);
}
print_sum(3 + 4);

Advanced mode can convert this to the following:

alert("The sum is 7");

The level of sophistication evident in this transformation speaks for itself.

But Advanced mode’s power comes at a price. Specifically, it imposes two major requirements:

  1. It must operate on the entire program, as a whole, at once. Otherwise, it cannot safely rename variables or enact other transformations.

  2. It only works on a subset of JavaScript. The exact restrictions can be found here. You don’t have to worry about this for ClojureScript code, since the ClojureScript compiler only emits compatible JavaScript. However, there are frequently issues with third-party libraries not designed with Google Closure in mind.

Consuming Libraries

A compelling feature of ClojureScript is that it is capable of utilizing any JavaScript library. Unfortunately, because of its reliance on Google Closure dependency management and advanced mode compilation, doing so is admittedly difficult. There are several different techniques, and choosing the wrong one will result in errors, or worse, subtle bugs in your application’s behavior.

Fortunately, it is possible to import and use any library safely, as explained below. See the flowchart at the end of the section for a high-level overview of the options you have.

ClojureScript Libraries

If the library you want to use is written in ClojureScript, your task is done. All you need to do is make sure the *.cljs files for the library are available on the classpath either as a JAR dependency or a source folder, depending on how the library is distributed.

Once the source files are on the classpath, you can :require or :use them exactly the same way you would a namespace that you wrote—in fact, from the point of view of the system, there isn’t any difference. They will be compiled and optimized along with your code.

JavaScript Libraries

You can also use libraries not originally intended for use with ClojureScript by using interop forms in your code to reference JavaScript variables (see Chapter 4).

How exactly to go about including such a library depends on the characteristics of the library in question. The procedure is different for libraries that were written with Google Closure in mind. For those that weren’t, there is another choice: compiling them in Advanced mode along with your application, or leaving them completely external.

Google Closure libraries

These are files that include a call to goog.provide(), allowing them to participate in Google Clojure’s dependency management system, and by extension, ClojureScript’s. A JS file that invokes goog.provide('x.y.z') effectively has the namespace x.y.z.

Unless you work at Google, you aren’t likely to see many libraries that fit into this category, as Google Closure doesn’t have a particularly large uptake in the JavaScript community. However, if you’re using a mixed JavaScript/ClojureScript codebase, and you’d like anyone to have the freedom to modify the library, you can consider making your JavaScript compatible with Google Closure to make it easier to use with ClojureScript.

First, to use them, you must start by putting the *.js files on the classpath so they are accessible to the compiler (just like ClojureScript files).

Second, you must tell the ClojureScript compiler where on the classpath the files are located. Unlike ClojureScript file paths, *.js pathnames don’t necessary have any relationship to the namespaces they provide. To indicate this to the compiler, use the :libs compiler option, which is a vector of *.js pathnames relative to the classpath. The compiler will inspect these files for calls to goog.provide() and handle them appropriately.

For example, to include a library in a file located at jslib/magic.js, the compiler options map might look something like this:

{:output-to "resources/public/js/main.js"
 :optimizations :advanced
 :libs ["jslib/magic.js"]}

Finally, to use these libraries in your ClojureScript code, :require or :use their namespaces in your ns declaration form. This will ensure that the library is available. Then, use ClojureScript’s JS interop to reference the JavaScript vars. Note that the namespace of a Google Closure library doesn’t always match the names of the variables it declares; that’s another ClojureScript convention that is stricter than what Google Closure alone requires.

Because libraries built for Google Clojure should already be compatible with Advanced mode compilation, you shouldn’t need to worry about preserving variable names against munging: Advanced-mode processing will be consistent across the entire codebase, including the required libraries.

Plain old JavaScript libraries

This is likely the most common type of library you might wish to use in ClojureScript: a normal, possibly popular JavaScript library that wasn’t written with ClojureScript or Google Closure in mind.

With these libraries, there is just one major choice to make: do you want to attempt to use the Google Closure Compiler’s Advanced Optimizations mode to compile the library together with your code, or include it separately on the HTML page? When you use Advanced mode, the Google Closure Compiler will perform whole-program optimization across both the library and your code. When you leave it separate, your program will still be compiled, but the external library will be loaded and run without any transformations.

As discussed above, compiling in Advanced mode has many benefits in emitted code size and runtime speed. Also, as you will see below, it’s slightly easier to use with ClojureScript. However, the fact is that most JavaScript code written without Advanced mode in mind probably doesn’t meet its rather stringent requirements.

If you do want to compile an existing library in Advanced mode, you should do a careful audit of the library’s code to make sure it meets the restrictions of Advanced mode. Be cautious: this is the one area where it is possible to go badly wrong. Sometimes incompatibilities don’t manifest in an obvious way until they’ve caused you serious trouble.

With Advanced mode

Compiling a library into your application with Advanced mode is fairly easy, given the major caveat that the library’s code is Advanced-mode compatible. The process is very similar to including a library that is built for Google Closure, the only difference being that instead of just telling the compiler the location of the file, it is necessary to tell it the location and the namespace it provides using the :foreign-libs compiler option.

:foreign-libs must be a sequence of maps, each containing a :file and a :provides key. The :file value is a path or URL indicating the absolute or classpath-relative location of the file. The :provides key is a vector of strings naming the namespaces that the file provides. Put together, it looks like this:

{:output-to "resources/public/js/main.js"
 :optimizations :advanced
 :foreign-libs [{:file "http://foo.com/foobar.js"
                 :provides ["foo.bar"]}]}

This will tell the ClojureScript compiler that when it’s concatenating all the sources prior to running them through the Google Closure Compiler, it should also include the source of the given file, and it should inject a call to goog.provide() in the source for each of the given namespaces.

The effect is the same as if the library had included a goog.provide() call on its own, and been included using the :libs option.

Again, this will only work if the library’s code conforms to the standards required by Advanced mode. Be especially aware that any nonconformities might not show up at compile time, although Google Closure will do its best to give warnings.

Without Advanced mode

This is likely to be the most common case: you have a library you want to use, but it wasn’t built for ClojureScript or Google Clojure, and you’re not confident it is compatible with advanced-mode compilation.

The basic premise for using such a library is very simple: just include it on your HTML page using a separate <script> tag, which will load it into the JavaScript runtime environment. Since your ClojureScript program has access to the root environment through its interop forms, you can directly reference any of the variables the external library has created using interop forms (such as the js/ pseudo-namespace).

Unfortunately, although this is fine for Whitespace Only or Simple Optimizations compilation, Advanced mode will cause errors without some extra work. The reason for this is variable munging. Advanced mode will potentially rename every symbol or property name mentioned in your ClojureScript code. If you’re compiling everything together with Advanced mode, the renaming will be consistent and everything will work. But if you’re only compiling your ClojureScript code and not the library, then things will be renamed with no way to match them back up, and you’ll end up with errors like “X is not a function” or “no such property X” errors.

For a real-world example, consider the following snippet of ClojureScript, which draws a circle using the excellent Raphael.js vector graphics library:

(let [image (js/Raphael. 10 50 320 200)]
    (.circle image 50 50 50))

This works great with Whitespace Only or Simple compilation. It uses the js/ namespace to call the global Raphael function as a constructor, which creates a Raphael drawing object of the specified dimensions. It then invokes the circle method on that object to draw a circle.

But try it in Advanced mode, and you’ll get a cryptic error, something like new Raphael(10, 50, 320, 200)).K is not a function. Initially, this doesn’t make sense: what is the K function it’s trying to invoke?

In this case, K is the munged name of circle. Google Closure doesn’t know that circle is a name that needs to be preserved. It has no knowledge of the Raphael library at all, since that wasn’t included in its compilation pass.

What’s needed is a way to inform the Google Closure Compiler that circle is an external reference, and should be left alone when compiling. Fortunately, Google provides such a mechanism: it allows you to create an externs file.

An externs file is just a JavaScript file that contains a JavaScript variable and property declarations. Any variable or property referenced in the externs file will not be munged. It isn’t actually compiled itself, so the variable references don’t have to be meaningful, they just have to be present to signal the compiler not to munge them. A very simple externs file that would make the Raphael code work would be something like this:

var Raphael = {};
Raphael.circle = function() {};

Of course, Raphael has many more functions than just circle, but you only need to declare the ones you want to consume from ClojureScript.

Finally, you need to tell the ClojureScript compiler about the externs file using the :externs compiler options, which is a sequence of strings of the classpath-relative paths of extern files. For example, to include this externs file for Raphael:

{:output-to "resources/public/js/main.js"
 :optimizations :advanced
 :externs ["raphael_externs.js"]}

That’s it! If you’ve included the necessary references in an externs file, then you can reference variables and properties from an outside context without munging, and successfully consume any JavaScript library you like.

Creating Libraries

It’s fairly straightforward to use ClojureScript to write libraries for distribution and consumption by other applications. You will need to package it differently, however, depending on whether you intend clients of your library to consume it using ClojureScript or JavaScript (Figure 7-1).

Flowchart for determining how to use a library in ClojureScript
Figure 7-1. Flowchart for determining how to use a library in ClojureScript

For Consumption by ClojureScript

The best way to distribute a ClojureScript library for use by ClojureScript is to distribute the *.cljs source files directly, either by giving clients a directory full of source code or distributing a JAR file containing the *.cljs files. In either case, the clients will need to add the directory or JAR to their compiler classpath so the ClojureScript compiler can find them.

All the client has to do to use the library is to consume it as described in the section above on consuming ClojureScript libraries.

For Consumption by JavaScript

If you want JavaScript applications to be able to call your ClojureScript code, you’ll need to distribute the compiled version of your app. Typically, the easiest way to do this is to compile your library to a single *.js file using the Google Closure Compiler and give that file to your clients. They can then reference that file in a <script> tag on their page, and start using it.

Fortunately, actually using the library should be fairly easy. ClojureScript functions are just JavaScript functions, and namespaces are just nested objects (following the JavaScript convention). So, for example, if you have a ClojureScript function foo.bar/hello-world, your JavaScript clients can easily call it using foo.bar.hello_world().

You will have to be careful not to expect arguments or return objects they won’t be able to use easily, such as ClojureScript vectors or maps. If you really intend your library to be used extensively from JavaScript, you’ll probably want to create a set of public API functions that accept and return more familiar types. For example, you might convert ClojureScript maps to JavaScript objects, sequences to JavaScript arrays, and keywords to strings before returning them.

^:export metadata

If you want to compile your libraries with Advanced mode, you’ll need to make one small additional change. JavaScript libraries can reference ClojureScript vars by name, but Advanced mode compilation munges all the var names. If you want to be able to reference a var from external JavaScript, you’ll need to mark it specifically for preservation.

To do this, tag the vars whose names you want to preserve using the :export metadata tag. ClojureScript metadata is data that can be attached to any ClojureScript object, and since it can be inspected by the compiler it can be used to alter the emitted JavaScript. In the case of :export, it indicates that the tagged var should not be munged.

Adding the :export metadata tag to a function using the metadata reader macro looks like this:

(ns foo)

(defn ^:export hello [name]
    (js/alert (str "Hello, " name)))

This code defines a very simple function called hello in the foo namespace, but tags it with :export so that it won’t be munged during advanced compilation. This means that a JavaScript caller can invoke it directly: foo.hello("Luke") would result in an alert box popping up that says “Hello, Luke”.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset