This article is Lesson 23 of Andrew's book, Learn ClojureScript
In ClojureScript, the unit of modularity is the namespace. A namespace is simply a logical grouping of functions and data that can be required and used by another namespace. External libraries expose modules as namespaces that we can require in our code, and we can (and should) break our code into multiple modules as well. In this lesson, we'll learn how to declare and require modules as well as how to use them to create clean architectural boundaries in our programs.
In this lesson:
- Organize functions and data into namespaces
- Architect a logical and intuitive project structure
- Work with namespaces at the REPL
Namespace Declarations
In each of the ClojureScript files that we have written so far, Leiningen has kindly generated a namespace declaration at the top of the file for us. We have used the (:require ...)
form to pull in third-party libraries, but we have judiciously avoided diving in to what exactly is happening here. That is about to change.
For the purpose of exploring namespaces in this lesson, let's create a new Figwheel project:
$ lein new figwheel import-fns
$ cd import-fns
If we look at the file that was generated in src/import_fns/core.cljs
, we see the following declaration at the top of the file:
(ns import-fns.core
(:require ))
The ClojureScript compiler will turn this declaration into a Google Closure Compiler module declaration, which will ultimately be compiled into a JavaScript object containing all of the functions and vars that we define in this namespace. From this bare-bones declaration, we can note a couple of things:
- We declare the name of the namespace as
import-fns.core
. This name must follow the naming convention discussed in Lesson 5. - There is a skeleton
(:require ...)
form that we can use to require code from other namespaces into this one.
Using Require
Since declaring a name is fairly self-explanatory, we will focus on the second item - requiring code from other namespaces. The :require
form in the ns
declaration is by far the most common way to require code from another file - including third party libraries - to make it available in our code1. Let's look at the various forms of :require
, using the project that we created above. We will again build on the example of a text-based adventure game, specifically displaying a player's inventory:
(ns import-fns.core
(:require import-fns.ui ;; <1>
[import-fns.format :refer [pluralize]] ;; <2>
[import-fns.inventory :as inventory] ;; <3>
[goog.dom :refer [getElement] ;; <4>
:rename {getElement get-element}]))
(defn item-description [i item]
(let [qty (inventory/item-qty i item)
label (if (> qty 1) (pluralize item) item)]
(str qty " " label)))
(let [i (-> (inventory/make-inventory)
(inventory/add-items "Laser Catapult" 1)
(inventory/add-items "Antimatter Scrubber" 5))]
(import-fns.ui/render-list (get-element "app")
(map (partial item-description i)
(inventory/list-items i))))
- Require the entire
import-fns.ui
namespace - Require a single function from the
import-fns.format
namespace - Require the
import-fns.inventory
namespace with the aliasinventory
- Require a single function from the Google Closure Library namespace, renaming that function
This snippet illustrates all of the common ways of requiring code into a namespace. After the :require
keyword, we can include any number of libspecs. The libspec may be either a fully-qualified namespace or a vector containing the namespace and some optional modifiers. This syntax is interpreted by the ns
special form to pull in vars from other namespaces according to the exact form of the libspec.
Form 1: Simple Namespace
In the first form, we simply use the name of a namespace that we would like to require: import-fns.ui
. We can then refer to any public var within this namespace by using the full namespace followed by /
and the name of the var, e.g. import-fns.ui/render-list
.
Form 2: Refer
In the second case, we follow the namespace by :refer [pluralize]
. :refer
will make every var that is listed in the vector that follows available without any prefix. Thus, we can write (pluralize item)
instead of (import-fns.format/pluralize item)
. Tny function in the import-fns.format
namespace can still be called using the fully-qualified syntax.
Form 3: Aliased Namespace
In the third case, we alias the namespace that we require using :as
. This works similar to the first case in that we are able to refer to any public var in the namespace. However, we can use the symbol that we specified after :as
instead of the full namespace. Thus, import-fns.inventory/make-inventory
becomes inventory/make-inventory
. The use of :as
can be very useful when requiring from namespaces with very verbose names. In general, we should also prefer :as
to :refer
, since it makes it clear where a var that we reference comes from without needing to look back at the namespace declaration.
Form 4: Aliased Vars
Finally, in the fourth case, we rename a specific var that we referred. Here, we alias the getElement
function from the Google Closure Library's goog.dom
module in order to give it a more idiomatic kebab-case
name. In practice, renaming is used more often to prevent a name collision. For example, we may want to import functions called mark-inactive
from both my-app.users
and my-app.customers
namespaces. We can use :rename
to import them as mark-user-inactive
and mark-customer-inactive
.
Require Form | Description | Function Usage |
---|---|---|
my.namespace |
require entire namespace | my.namespace/compute |
my.namespace :refer [compute other-fn] |
require specific functions and refer to them unqualified | compute |
my.namespace :as mine |
require namespace with alias | mine/compute |
my.namespace :refer [compute] :rename {compute calculate} |
require namespace with specific vars aliased | calculate |
Forms of Require
Note:
There is a form,
:refer-clojure
that allows us to rename a var from the ClojureScript standard library (or exclude it altogether) in the event that we want to define a var in our namespace with the same name.(:refer-clojure :rename {str string})
will alias thestr
function asstring
, so that we are free to reuse thestr
name without generating any warnings. Similarly, we can exclude thestr
function altogether with,(:refer-clojure :exclude [str])
. That said, we should usually be giving things names that are descriptive enough that they do not conflict with anything in the standard library.
Importing Google Closure Library Classes
In the case that we are importing a class constructor from the Google Closure Library, there is one form that we should be aware of in addition to :require
- that is :import
. This form exists only for bringing a class constructor from a Google Closure module into our namespace in the same way that the :refer
keyword in a libspec lets us bring in a var from another namespace. Like :require
, :import
expects to be followed by a number of import specs. The most common form is a vector with the namespace from which to import classes followed by the unqualified constructor names to import from that namespace:
(ns my-ns
(:import [goog.math Coordinate Rect]))
(.contains (Rect. 10 50 5 5)
(Coordinate. 12 50))
The thing to remember about :import
is that it is only used for requiring classes (including enums) from Google Closure modules - never for including a ClojureScript namespace or a (non-constructor) function from a Google Closure module.
Requiring Macros
One of ClojureScript's most powerful features is its macro support. However, the way in which macros are implemented presents a bit of a quirk in the way that we have to require them. A macro has access to the ClojureScript code that is "inside" its call site as raw data (lists, vectors, symbols, etc.), and it can manipulate this code as data structure before returning the code that actually gets compiled to JavaScript. The tricky thing is that since the ClojureScript compiler is written in Clojure, the macros themselves are Clojure code that generates ClojureScript code. We will learn more about macros in a later lesson, but for now the important thing to remember is that since macros are Clojure code, we need to go about requiring them another way. That way is the :require-macros
form:
(:require-macros [macro-ns :as macros])
This form functions very much like :require
with the exception that it works with importing macros from Clojure namespaces rather than vars from ClojureScript namespaces.
Macro Compilation
Grouping Related Functions
Now that we have discussed the mechanics of declaring a namespace and requiring other code that we would like to use, let's take a step back and look at how to organize our namespaces. One of the features of ClojureScript that can be both liberating and frustrating is that it is not opinionated about how namespaces are structured. We can put as little or as much into a namespace as we would like, and as long as we do not create circular dependencies, all is well. Some of the insights from object-oriented programming can also help us here: high cohesion and low coupling.
High cohesion means that if two pieces of code are closely related in purpose, they should be close in the architecture of the program - usually in the same namespace. Low coupling is the other side of the same coin: if two pieces of code are unrelated, they should be distant from each other in the architecture of the program. Unlike OOP practitioners, we extend this principle to data as well. Since hiding data is not a goal of functional programming, we tend to group functions and the data that they operate on into the same namespace and expose both freely.
Let's return to the adventure game inventory code by way of example. In the import-fns.core
namespace, we require the import-fns.inventory
and import-fns.ui
namespaces as well as pluralize
from import-fns.format
. These files contain the business and presentation logic for this little app. First, we will look at import-fns.inventory
:
(ns import-fns.inventory) ;; <1>
(defn make-inventory [] ;; <2>
{:items {}})
(defn- add-quantity [inventory-item qty] ;; <3>
(update-in inventory-item [:qty]
(fn [current-qty] (+ current-qty qty))))
(defn add-items
([inventory item] (add-items inventory item 1))
([inventory item qty]
(update-in inventory [:items item]
(fnil add-quantity
{:item item :qty 0}
qty))))
(defn list-items [inventory]
(keys (:items inventory)))
(defn item-qty [inventory item]
(get-in inventory [:items item :qty] 0))
src/import_fns/inventory.cljs
- All functions in this file will be part of the
import-fns.inventory
namespace - Declare a public function named
make-inventory
- Use
defn-
to declare a private function
As the name suggests, this namespace contains all of the code relating to creating and managing an inventory. It has no formatting or display logic. We expose make-inventory
for constructing a new inventory, add-items
for adding some quantity of a specific item, list-items
for getting the distinct items in the inventory, and item-qty
for getting the quantity of a particular item. We make the add-quantity
function private by declaring it with defn-
because it exists solely as an implementation detail of add-items
and serves little value outside this context. Next, let's look at the import-fns.format
namespace:
(ns import-fns.format
(:require [clojure.string :as s]))
(defn ends-with-any? [word suffixes]
(some (fn [suffix]
(s/ends-with? word suffix)
suffixes)))
(defn replace-suffix [word old-suffix new-suffix]
(let [prefix-len (- (count word)
(count old-suffix))
prefix (.substring word 0 prefix-len)]
(str prefix new-suffix)))
(defn pluralize [word]
(cond
(ends-with-any? word ["s" "sh" "ch" "x" "z"])
(str word "es")
(s/ends-with? word "f")
(replace-suffix word "f" "ves")
(s/ends-with? word "fe")
(replace-suffix word "fe" "ves")
(re-find #"[^aeiou]y$" word)
(replace-suffix word "y" "ies")
:else (str word "s")))
src/import_fns/format.cljs
Again, this namespace contains all of the string formatting functions that we need for the app. Even though we only require pluralize
in our main namespace, we make all of the functions public since they all contain reusable logic. Additionally, when we expose every function, we make it possible to test every function in isolation. Note that all of these function are general functions that will work with any string. If we had code that was specific to inventories, we would put it in the inventory
namespace. So what should we do if we have a set of functions that are formatting functions specific to inventories? One common practice is to create a new namespace called something like import-fns.format-inventory
, and this namespace could require functions and data from both formatting and inventory namespaces to perform its specialized work. When we do not have to squeeze the architecture of our code into a class hierarchy, it gives us a great deal of flexibility. Finally, for the sake of completeness, let's look at the import-fns.ui
namespace:
(ns import-fns.ui
(:require [goog.dom :as gdom]))
(defn append-list-item [list text]
(gdom/appendChild list
(let [li (gdom/createElement "li")]
(gdom/setTextContent li text)
li)))
(defn render-list [elem items]
(let [ul (gdom/createElement "ul")]
(doseq [item items]
(append-list-item ul item))
(gdom/removeChildren elem)
(gdom/appendChild elem ul)))
src/import_fns/ui.cljs
There is not much that is novel to note about this namespace, except that its API is formed around rendering text rather than an inventory specifically. The "glue code" that ties inventories and rendering together is all in our core namespace.
Avoiding “Hidden” OOP
One thing that many new ClojureScript developers will realize is that with atoms and namespaces that can contain private variables, we can easily emulate stateful objects. This is true - we can define a private var to hold our object state within an atom, then we can use public functions to define the API and private functions as implementation details. However, as soon as we introduce hidden state, we lose the benefits of functional purity and referential transparency in terms of both testability and reasonability.
Namespaces and the REPL
Before moving on, let's look briefly at how to interact with namespaces from the REPL. When we used :require
or :import
in the namespace declaration, the ns
special form used these directives to determine what code to make available within our namespace. There are macros that act as analogs to these directives - require
and import
- which we can run from the REPL to expose code to our REPL session:
dev:cljs.user=> (require '[import-fns.format :as fmt] :reload)
nil
dev:cljs.user=> (fmt/pluralize "burrito")
"burritos"
The primary difference is that we must quote the vector that we pass to require
- otherwise, the REPL would try to resolve the import-fns.format
and fmt
symbols to vars that are bound in our current namespace. Quoting the vector (i.e. using '[]
instead of []
) causes ClojureScript to interpret each symbol in it as a literal symbol rather than attempting to resolve them. An additional difference is that we can add the :reload
keyword after the require spec to cause the REPL to reevaluate the namespace in order to pick up on any changes that were made to the file since we last required it.
By default, when we start up a REPL, it operates inside a namespace called cljs.user
. Anything that we def
(or defn
) will be defined inside this namespace for the duration of the REPL session. If we would like to operate inside another namespace, we can use the in-ns
special form to instruct the REPL to operate within another namespace instead:
dev:cljs.user=> (in-ns 'import-fns.format)
dev:import-fns.format=> (replace-suffix "programmer" "er" "ing")
"programming"
Using in-ns
can simplify our workflow when we are practicing REPL-driven development. We can start writing our code for a namespace in the REPL then copy all of our definitions to the file when we are satisfied with how they work.
Summary
In this lesson, we learned what namespaces are as well as how to declare a namespace with its dependencies using :require
and :import
. We also looked at how to think about organizing our code into namespaces using the principles of high cohesion and low coupling. Finally, we looked at how to interact with namespaces from the REPL so that we can effectively navigate and test our code.
-
There is another form,
(:use [namespace :only [var1 var2]])
that works just like(:require [namespace :refer [var1 var2]])
. It used to be used heavily in Clojure before:refer
was available inside(:require)
, but it has all but fallen out of use. ↩
Top comments (0)