There are Clojure dialects (Clojure, ClojureScript, ClojureCLR) on several different platforms. We wish to write a single source file that runs on more than one of these platforms, retaining all of the common code and factoring out only the platform-specific bits.
Use cases for platform-specific functionality:
- Platform-specific require/import (the most common case)
- Exception handling (catch "all" is platform-specific - see http://dev.clojure.org/jira/browse/CLJ-1293 about this specific case)
- Platform-specific calls for strings, dates, random numbers, uris, reflection warnings (hosty things)
- Protocol implementations have leading dash in cljs so name is different
- Extending protocol to implementation-specific classes (clojure.lang.IPersistentVector)
- Math - casting or other math stuff specific to ClojureScript
Proposal: Feature expressions
Common Lisp approaches this problem using feature expressions.
Each platform has a variable called
*features* that is a set of keywords to indicate supported features. The initial set of well-known features will indicate the platform: :clj, :cljs, :clr. Users may introduce a broader set of features or the well-known set may be expanded at a later date. User-supplied features should be namespaced.
The Reader understands a new kind of "feature expression". The reader macros #+ and #- are used to include or skip a form based on a feature expression:
- #+feature-expr form - include next form if feature-expr evaluates to true
- #-feature-expr form - skip next form if feature-expr evaluates to true
Feature expressions evaluate as booleans and are defined as follows:
feature - a symbol, evaluated as if:
- (not feature-expr) - returns logical not of feature-expr
- (and feature-expr*) - returns true if all feature-expr are true, otherwise false.
- (or feature-expr*) - returns true if any of its feature-expr are true, otherwise false.
Skipping in the reader is performed by binding *read-suppressed* to true for the next form.
Reading Unreadable Things
There may be tagged literals or classes that are not known or available on all platform. The reader must be able to read but avoid constructing these entities.
The #js tagged literal is not known on the Clojure platform, but the reader should read and skip it without failure.
Potential issue: Clojure CLR needed to expand the valid set of symbols and uses the #| reader extension from Common Lisp to support this in the CLR. The corresponding symbols are not currently readable by Clojure or ClojureScript. The #| extension support is described here: https://github.com/clojure/clojure-clr/wiki/Specifying-types. The #| reader extension may need to be supported in Clojure and ClojureScript to support the full set of valid ClojureCLR symbols in feature expressions.
Loading and File Extensions
When code needs to be loaded based on a namespace:
- Clojure will continue to read only .clj files.
- ClojureScript will first look for .cljs files, then for .clj files. The .clj file will be read as by the reader as a ClojureScript file.
- ClojureCLR - ???
Copy / paste
One approach is to maintain two versions of the same file that are largely the same but modify the platform-specific parts in each copy. This obviously works but is gross.
cljx is an implementation of feature expressions that:
.cljxfiles containing feature expression-annotated code into external files based on well-known tags
- The most common use is to use clj and cljs tags and write
.cljsfiles for consumption by other tools/compilers/etc
- The most common use is to use clj and cljs tags and write
- optionally applies the same transformation interactively via installation of a REPL extension
It has been used successfully by a number of projects (see the cljx README for a partial list). cljx's limitations include:
- It does not address portability of macros at all; it is strictly a source-to-source transformation. Macros continue to be written in Clojure, and must be rewritten or implemented conditionally on the contents of
- It does not provide any runtime customization of the "features" you can use; these are set either within build configuration (via cljx' Leiningen integration), or via the configuration of the cljx REPL extension. The latter technically is available for modification, but is not in practical use.
- The set of provided "features" is limited to one for Clojure (
#+clj) and one for ClojureScript (
#+cljs). Further discrimination based on target runtime (e.g. rhino vs. node vs. v8 vs. CLR) would be trivial, but has not been implemented to date.
cljx expressions are typically applied:
- Inside ns macro
- Top-level forms
- Occasionally internal forms where it's concise
- (.getTime #+clj (java.util.Date.) #+cljs (js/Date.))
lein-cljsbuild provides a (deprecated, to be removed) feature called "crossovers" that provides a very limited preprocessing of certain files during the cljsbuild build process; a special comment string is removed, allowing one to work around the
-macros declarations required in ClojureScript
ns forms. Crossover files must otherwise be fully portable. Language/runtime-specific code must be maintained in separate files. However, (my) experience shows that this can quickly lead to the situation where one has to think a lot about in which file to put a specific function, in order to go though the whole preprocessing machinery. Functions are split into namespaces because of conditional compilation, and not because they belong to the same part or module of the program.
Define a custom tagged literal that implements conditional read-time expressions:
#feature/condf [ (and jdk-1.6+ clj-1.5.*)
else (some-old-fashioned-code) ]
Proof of concept here: https://github.com/miner/wilkins
The Common Lisp Hyperspec about the Sharp Sign macros:
Examples of Common Lisp's Feature Expresions:
- http://cliki.net/features - list of Common Lisp "features" and what they mean
Maintaining Portable Lisp Programs:
Crossover files in lein-cljsbuild:
ClojureScript JIRA Tickets and patches with a proof of concept implementation of CL's feature expressions: