Combining Clojure and ClojureScript Libraries

Combining Clojure and ClojureScript Libraries

Andrew Zures
Andrew Zures

March 08, 2014

ClojureScript is an incredible new project that any Clojurian can appreciate. What could be better than replacing vexing JavaScript with the clean, flowing syntax of Clojure? But ClojureScript is very new, and in some cases its lack of polish can be frustrating. One of these frustrating aspects is ClojureScript's failure to complete abstract JavaScript using Clojure syntax. This comes from the fact that ClojureScript does not currently have all the functionality of its counterpart. For example, Clojure's bound? function is not implemented in ClojureScript (see "bound?" in ClojureScript devnotes). That may seem trivial but if you're hoping to transfer a piece of Clojure code to ClojureScript, and that code has a bound? call, you must learn additional aspects of ClojureScript to get your code to work. You will also have to maintain the new ClojureScript version of your code along with your pre-existing Clojure version. These small differences can add up and before you know it you could be maintaining two separate Libraries. The kicker is of course that these libraries have about 95% code in common but the 5% of difference simply cannot be resolved.

We encountered this problem here at 8th Light while extending our Clojure testing framework Speclj to ClojureScript. There were numerous ClojureScript incompatibilities sprinkled throughout the existing Clojure library. In our original solution, we built a pre-compiler that would run against the Clojure code. This program could switch out all of the incompatibilities with their ClojureScript equivalents. However, the solution came with a few major drawbacks. First, the pre-compiler was a one-off design, built specifically for the Speclj project. There was little chance that it would be maintained, which introduced an added layer of fragility to our project. More importantly, the ClojureScript version still was its own library. This led to the confusing distinction between Speclj (Clojure) and Specljs (ClojureScript). Both libraries had to be added independently to a project.

Recently, we took another look at our two massively similar libraries and asked if, with the changes to the Clojure/ClojureScript landscape, we could do better. Not to spoil the fun, but we were able to combine our two libraries into one. Unfortunately it did take a few absolutely-necessary hacks but that is the cost of working with such an exciting, constantly changing technology like ClojureScript. We definitely look forward to the day when ClojureScript becomes an effortless abstraction of JavaScript but until then a little creativity will have to suffice.

Setting Up Your Base Project:

A Sample Base Project is available if you would like to use it. It already has Speclj installed and running for both Clojure and ClojureScript. It also has branches with working code for each part of this tutorial. We will be referencing this project throughout this tutorial.

Regardless of whether you use the sample project, your project structure should look similar to its structure. This is especially true for your src/ and spec/ paths. You will want a structure that resembles src/file-type/project-name/code.file-type. For example, in the sample project we have src/clj/myproject/core.clj for Clojure source and src/cljs/myproject/core.cljs for the ClojureScript source.

A similar structure should be used for your tests.

myproject
|
|-- test
| |-- clj
| | |-- myproject
| | | |-- test-code.clj
| |-- cljs
| |-- myproject
| |-- test-code.cljs
|-- src
				|-- clj
				| |-- myproject
				| |-- source-code.clj
				|-- cljs
								|-- myproject
												|-- source-code.cljs

Dividing Your ClassPaths with Profiles:

The Clojure and ClojureScript versions of your project, although residing within the same jar, will have different classpaths. It is important that you isolate these classpaths in a way that will allow you to test and run the two portions of your library independently. This can be done by adding separate profiles to your project.clj file. Let’s make :clj and :cljs profiles.

Both profiles will use the org.clojure/clojure and speclj dependency so they should remain outside the :profile map. That means that all we need for the :clj profile is:

{
		:source-paths ["src/clj"]
		:test-paths ["spec/clj"]
}

For the :cljs profile, we will need the standard :cljsbuild information. Additionally, the sample base project uses speclj for testing so we will see some syntax necessary for Speclj as well.


:cljs {:dependencies [[org.clojure/clojurescript "0.0-2014"]
																						[org.clojure/tools.reader "0.7.10"]
																						[lein-cljsbuild "1.0.2"]]
							:plugins [[lein-cljsbuild "1.0.2"]]

							:cljsbuild ~(let [run-specs ["bin/speclj" "target/tests.js"]]
																					{:builds {:dev {:source-paths ["src/cljs" "spec/cljs"]
																																					:compiler {:output-to "target/tests.js"
																																																:pretty-print true}
																																					:notify-command run-specs}}
																						:test-commands {"test" run-specs}})}

Now let make our lives easier by adding testing aliases to our project. Here is the alias to run your Clojure speclj tests using the :clj profile:

:aliases {
		"clj-test" ["with-profile", "clj", "spec"]
}

The ClojureScript testing alias looks very similar:

:aliases {
		"clj-test" ["with-profile", "clj", "spec"]
		"cljs-test" ["with-profile", "cljs", "cljsbuild", "test"]
}

Now, if you have Speclj configured correctly, you can run lein clj-test and lein cljs-test from the command line to run your Clojure and ClojureScript tests respectively.

Working With CLJX:

sample code through CLJX

Next, we will use the CLJX library. CLJX translates a .cljx files into separate .cljs and .clj files. You can use small #+cljs and #+clj tags to differentiate which forms you would like included in which version. In the 8th Light Speclj project, CLJX replaced our hand-rolled pre-compiler. This gave us the benefit of relying on an open-source, updated dependency instead of our own program.

However, CLJX comes with a few downsides. First, you will have to keep track of the status of your cljx results. If you make a change in a .cljx file and, for whatever reason, do not recompile the cljx folder, your changes will not appear in your .clj and .cljs files. Second, You should be careful to not make changes to the generated .clj and .cljs files since they will be overridden the next time you generate your cljx output. Third, if you are running a test autorunner, it will likely not pick up changes to .cljx files.

So CLJX comes with a cost, but it does allow you to keep a relatively similar code base for your Clojure and ClojureScript libraries.

Adding CLJX to your Project.clj

To add CLJX simply add [com.keminglabs/cljx "0.3.1"] to your general :dependencies map. You will then have to configure source and output paths in the :cljx key:

:cljx {:builds [{:source-paths ["src/cljx"]
											:output-path "target/generated/src/clj"
											:rules :clj}
										{:source-paths ["src/cljx"]
											:output-path "target/generated/src/cljs"
											:rules :cljs}
										{:source-paths ["spec/cljx"]
											:output-path "target/generated/spec/clj"
											:rules :clj}
										{:source-paths ["spec/cljx"]
											:output-path "target/generated/spec/cljs"
											:rules :cljs}]}

So now every .cljx file in our src/cljx folder will be translated to separate .clj and .cljs version, which will be stored in the target/generated/src/ folder.

It also will help to add the cljx hooks so that cljx will automatically build your files when you attempt a normal leiningen command:

:hooks [cljx.hooks]

Configuring Your Source-Paths and Test-Paths

With cljx installed, we will have to change our source-paths and test-paths so that they look for the files generated by cljx.

For your :clj profile, simply modify the paths like below:


{
		:source-paths ["src/clj", "target/generated/src/clj"]
		:test-paths ["spec/clj", "target/generated/spec/clj"]
}

For your :cljs profile you new source and test resources will both go in :source-paths collection:


{
		:source-paths ["src/cljs"
																	"spec/cljs"
																	"target/generated/src/cljs"
																	"target/generated/spec/cljs"]
		:compiler {:output-to "target/tests.js"
													:pretty-print true}
		:notify-command run-specs
}

Making Things Easy with Aliases

We're almost done. With cljx hooks in your project.clj, cljx will auto-generate files before you run you Clojure tests. However, for ClojureScript we'll have to test leiningen to compile before we run tests. We can do this easily by changing our cljs-test alias:


:aliases {
		"cljs-test" ["do" "cljx" "with-profile" "cljs" "cljsbuild" "test"]
}

We also are little more concerned about the target directory. So we may want to clean that directory before regenerating our cljx code. We can add a few aliases that help us with that:


{:aliases
	{"clj-clean-test" ["do" "clean," "clj-test"]
		"cljs-clean-test" ["do" "clean," "cljs-test"]}}

Lastly, we can add a final alias that will run both our clj and cljs tests in one command:


:aliases {
		"all-tests" ["do" "clean," "clj-test," "cljs-test"]
}

Now our project.clj file is updated. We should now be able to add a .cljx file to our project and it will generate separate but similar .clj and .cljs files.

Adding a .cljx File to your Project

If you are using the sample project you will see that we already have src/clj/myproject/core.clj and src/cljs/myproject/core.cljs. We will create a similar directory structure for the .cljx files.

Let us make a src/cljx/myproject/ folder and add shared_file.cljx to the new folder. Next, let us make a spec/cljx/myproject/ folder and add shared_file_spec.cljx.

In shared_file.cljx add:


(ns myproject.shared-file)

(defn multiply [x y]
		(* x y))

*

In shared_file_spec.cljx add:


(ns myproject.shared-file-spec
		#+clj :require #+cljs :require-macros [speclj.core :refer [describe it should=]]
		:require [speclj.core]
											[myproject.shared-file :as shared-file])

(describe "sample cljx file"

		(it "uses cljx files to generate tested code in clj an cljx"
				(should= 12 (shared-file/multiply 3 4))))

As you can see, the spec file includes #+clj and #+cljs tags. For this file, that means that cljx will add :require to the clojure version of the file and :require-macros to the Clojure version of the file. These tags will include or exclude the entire form that follows them. So one tag can include/exclude an entire function.

As a side note, the :require-macros key is the way we get access to our Clojure macros in ClojureScript. Since Speclj uses macros for both Clojure and ClojureScript the small #+ tags let us properly import the macros for both platforms.

Run our Tests with A CLJX file

Now that we have cljx set up and a .cljx file and test file, we can run lein clj-clean-test and lein cljs-clean-test. Both should evaluate the multiply test included in the single .cljx file in their respective platforms by testing against the cljx generated code.

Now we have a single .cljx source and spec file that will be generated into separate .clj and .cljs files. And we can test our code in both Clojure and ClojureScript.

Using Platform Files to Isolate Library Differences

sample code through Platform Files

Now that we can write a single file that ultimately becomes separate .clj and .cljs files, we can look at how we're going to deal with the differences in the Clojure and ClojureScript platforms.

We will isolate these differences in two files (one .clj and one .cljs) with the same file name and same namespace name. We will then reference this common namespace in our code. When our clojure code runs, the .clj namespace will execute, and when we run our ClojureScript code, our .cljs namespace will execute. Thus the rest of our files can be written without a need to focus on platform details.

An example will illustrate the project design:

Platform Files: An Example

Let's say we want to use our platform's abs function to find the absolute value of a number. In Clojure this is done using the org.clojure/math.numeric-tower library, while ClojureScript would use JavaScript's Math/abs function.

To set up this example, we'll make a .cljx function that uses abs

Lets add an "absolute difference" functionality to shared_file.cljx. This functionality will simply find the absolute difference between two numbers.

As always, we'll start with a test. Add the code below to the describe block in your shared_file_spec.cljx:

(it "finds the absolute difference between two numbers"
		(should= 1 (shared-file/abs-diff -101 100)))

This test will simply help us decide if everything is working correctly. Now let's focus on the source code.

Add the code below to your shared-file.cljx:

(ns myproject.shared-file
		(:require [myproject.platform :as platform]))

And add your new function, which should look like this:

(defn absolute-difference [x y]
		(- (platform/absolute-val x) (platform/absolute-val y)))

As you can see, we're referencing a platform namespace. This namespace will change based off of what platform we're executing. Let's now make our platform namespace files.

Create platform.clj in src/clj/myproject/ and platform.cljs in src/cljs/myproject/

In platform.clj add:


(ns myproject.platform
		(:require [clojure.math.numeric-tower :as math]))

(defn abs [num]
		(math/abs num))

You'll also have to add the numeric-tower dependency to your :clj profile in project.clj:

:dependencies [[org.clojure/math.numeric-tower "0.0.4"]]

Now let's move to the cljs side. In platform.cljs add:


(ns myproject.platform)

(defn abs [num]
		(js/Math.abs num))

Now we have two like-named functions in two like-named namespaces. If we run our tests, they'll pass. This works because when Clojure runs, the platform.clj file will be used and the numeric-tower library will be executed. When ClojureScript runs, the platform.cljs file will be used and JavaScript's abs function will be executed. Our shared-file doesn't need to know about those details. It can simply call the platform namespace's function and receive the results.

We now have a function, abs-diff that is written just once yet can be used for both Clojure and ClojureScript. This means that not only can we write a single code base that can run in Clojure and ClojureScript, it also means that the differences between the two platforms is isolated to the platform namespace.

Using Platform Files with Macros

sample code through Platform Files with Macros

In the last section we used separate .clj and .cljs files with the same namespace name to isolate the platform differences between Clojure and ClojureScript. But what if we want to use these files in macros? You can still use these files in macros but there are a few tweaks needed to our previous platform strategy.

The issue with macros is that there will be no equivalent .cljs macro file. Most macros will stay on the Clojure side of your project. Let's take a look at an example.

Platform Files and Macros: An Example

First, we need to tweak your platform.clj file. Add the snippet below to your :cljs profile. That way your Clojure macro will be available to your ClojureScript profile.

:source-paths ["src/clj"]

Now let's add a new test to our shared_file.cljx file which will test a simple macro.

Create a macro.clj file in your src/clj/myproject/ folder. You can leave it empty for now.

Next, add the dependency to your shared_file_spec.cljx under :require-macros. Your namespace should now look like this:

(ns myproject.shared-file-spec
		#+clj :require #+cljs :require-macros
		[speclj.core :refer [describe it should=]]
		[myproject.macros :as macros])
(:require [speclj.core]
										[myproject.shared-file :as shared-file])

For this example, we'll add our existing platform/abs functionality to a macro. But tests come first! Add a new test that will evaluate an abs function located in your macro file:

(it "finds absolute value using macro"
		(should= 2 (macros/abs -2)))

So now we have a test but it won't pass since we have nothing in our macro.clj file

Let's now add a namespace and simple abs function to macro.clj.

(ns myproject.macros
		;(:require [myproject.platform]) ;uncommenting will break cljs tests
)

(defmacro abs [x]
		`(myproject.platform/abs ~x))

Now here is where things get interesting. As you can see we've commented out the :require statement and our macro function uses a fully qualified namespace. It seems like our macro might not be able to find the platform namespace. But let's run our tests.

They should pass!

Now uncomment the :require statement and rerun your tests. Your ClojureScript tests will fail to run!

It seems like we should :require our platform namespace since the file uses it, but this will actually break the ClojureScript-side tests. This is because the macro file will always attempt to evaluate the .clj version of our platform file if it is defined in the :require namespace. By using a fully-qualified namespace in our macro instead of referencing it through a :require statement, the correct platform file will be evaluated at macro expansion. So, when it comes to macros, you should not :require the platform file but instead use its fully-qualified namespace where it is needed.

Now we've seen how to use platform files to isolate platform difference in both normal functions and clojure macros. These platform files can get you far, but they don't get you all the way there. In the next part of the tutorial we'll see how to use an ugly but effective "if" statement to get essentially complete cross-platform functionality.

Powerful but Perilous Context-aware Macros

sample code through Context-aware Macros

In the previous portion of this tutorial, we were able to get a great deal of cross-platform functionality using platform files. But now we'll look at a way to define a clojure macro with platform-specific code, using a very fragile if statement.

First, let's look at an example:

Context-aware Macros: An Example

In your shared_file_spec.cljx files add this test:


(it "catch slurp failure"
		(should= true (macros/slurpable-file? "badfilename")))

In this test, we're attempting to slurp a bad file name. In both Clojure and ClojureScript, this will raise an exception. But exceptions are a little different in Java and JavaScript. Java will require an Exception while JavaScript will use a js/Ojbect. You can see the [Clojure documentation][clojure_documentation] for an example of both situations.

Let's go to our macros.clj file and see what we can do to pass this test for both platforms.

Here's what the Clojure version might look like. But of course it won't work in ClojureScript. ClojureScript won't know what to do with Exception.

(defmacro slurpable-file? [file-name]
		`(try
				(slurp ~file-name)
				(catch Exception e# true)))

Here's what the ClojureScript version might look like. But of course it won't work in Clojure. Clojure won't know what to do with js/Ojbect.

(defmacro slurpable-file? [file-name]
		`(try
				(slurp ~file-name)
				(catch js/Object e# true)))

Finding the Context

We have two separate macros that simply won't work on both platforms. But what if we could determine if the macro.clj file was expanding in a Clojure context or a ClojureScript context. Maybe then we could use this information to build the correct macro for the currently-executing library.

This is where a fragile "if" statement comes into play. It looks like this:


(defn cljs? []
		(boolean (find-ns 'cljs.analyzer)))

As you can see it determines if the file is running in a ClojureScript context if the cljs.analyzer namespace can be found. Otherwise, we will assume it is in a Clojure context. We can use this function to create a macro that can combine our two previous macros.

(defmacro slurpable-file? [file-name]
		`(try
				(slurp ~file-name)
				~(if (cljs?)
						'(catch js/Object e# true)
						'(catch Exception e# true))))

If we try this macro, it will pass the tests for both platforms. It does this by adding the correct platform-specific catch statement during macro expansion. Thus we have a macro that, to some degree, is away of the context of its expansion. This may seem like it has opened up an amazing set of functionality but the if statement is fragile. It relies on the existence (or lack thereof) of a ClojureScript specific namespace. If something changed in ClojureScript, the entire library could fail. Thus using the cljs function above is a bit of hack. But it gets us where we want to go and there are few other options.

ns-resolve Can Help Too

As a brief side note, the hack noted above will still fail if the Clojure compiler cannot recognize the name of a currently absent ClojureScript namespace. An example is cljs.compiler/munge. If this included in a .clj macro, your Clojure-side tests will fails because Clojure will not find the namespace.

However we can get around this using ns-resolve. Instead of referencing cljs.compiler/munge we can replace it with:

1 (ns-reolve 'cljs.compiler "munge")

This will essentially push the cljs.compiler namespace check to runtime. If you have your macro setup correctly, the cljs namespace should never be called in Clojure.

So now we've seen how to use platform files to isolate platform-specific code and we've also seen a little hack that can help when macros must be defined in a platform-specific manner. In the next part of this tutorial we'll put it all together, quite literally, and combine our Clojure and ClojureScript libraries into a single jar.

Adding clj and cljs Code to a Single Jar

sample code through Single Jar

In the previous parts of this tutorial we've built a code base that can deliver the same functionality in both Clojure and ClojureScript. Now we'll see how we can deploy this functionality in one Jar. This gives others the ability to import one library and gain both your clj and cljs functionality.

Let's go to our project.clj file. We'll add an entirely new profile called "combined"

:profiles {
		:combined {
				:dependencies [[org.clojure/math.numeric-tower "0.0.4"]
																			[org.clojure/clojurescript "0.0-2014"]
																			[org.clojure/tools.reader "0.7.10"]
																			[lein-cljsbuild "1.0.2"]]
				:source-paths ["src/clj", "target/generated/src/clj"]
				:resource-paths ["src/cljs", "target/generated/src/cljs"]
				:test-paths ["spec/clj", "target/generated/spec/clj"]
				:cljsbuild ~(let [run-specs ["bin/speclj" "target/tests.js"]]
																	{:builds {:dev {:source-paths ["src/cljs"
																																			"spec/cljs"
																																			"target/generated/src/cljs"
																																			"target/generated/spec/cljs"]
																																	:compiler {:output-to "target/tests.js"
																																												:pretty-print true}}}
																		:test-commands {"test" run-specs}})
		}
}

This profile effectively combines our :clj and :cljs profiles into one. You'll note that all of the dependencies or both profiles are added to the :combined profile.

We've also added :resource-paths with src/cljs and target/generated/src/cljs. This will add our ClojureScript files to our jar, while the normal :source-paths will add our Clojure code.

So let's test this new, combined profile. Here, an alias will be helpful:

"combined-tests" ["do" "clean," "with-profile" "combined" "spec," "with-profile" "combined" "cljsbuild" "test"]

Now if we run lein combined-tests everything should pass!

If you lein with-profile combined jar and go to your target/ file, you can jar tf the generated .jar file and see how both the .clj and .cljs file are combined into the same library.

All we have to do now is install our project using the combined profile. Again we'll use an alias to make things easy:

"install" ["do" "clean," "with-profile" "combined" "install"]

Now we can lein install and we will have a single library that will work for both Clojure and ClojureScript.

That's the end of the tutorial. I hope you enjoyed it!