Error formatting macro: pagetree: java.lang.NullPointerException
Skip to end of metadata
Go to start of metadata
You are viewing an old version of this page. View the current version. Compare with Current  |   View Page History

Problem

There are several Clojure implementations (Clojure, ClojureScript, ClojureCLR). The problem we wish to solve is how to write a single source file that runs on more than one of these platform implementations, retaining all of the common code and factoring out only the platform-specific bits.

Known 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

There are other potential uses for conditionally including code based on factors other than platform (host architecture or presence of some external capability).

Some Solutions

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

cljx is an implementation of feature expressions that:

  • rewrites .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 .clj and .cljs files for consumption by other tools/compilers/etc
  • 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 &env.
  • 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 "crossovers"

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.

Tagged Literal

Define a tagged literal #feature[<feature expr> <value>]. 

Proof of concept here:  https://github.com/miner/wilkins

Proposed Solution: feature expressions

A solution to this problem that is used by Common Lisp implementations are feature expressions. Each platform has a variable called *features* that contains keywords that indicate the supported features of the platform the code is running under. The branching on a platform or a platform specific feature is done via the reader macros #+ and #- followed by a feature condition. The feature condition is either a symbol or a form that combines symbols with the or, and or not operators. The feature condition is evaluated by looking up the symbols in the *features* variable. If the feature condition evaluates to true the next form will be passed through the reader and evaluated, otherwise it will be discarded.

The patches attached to the CLJS-27 ticket contain a proof of concept implementation of these feature expressions for Clojure and ClojureScript. With this extension one can branch on a form level and write code like illustrated by the following example:

(ns feature.expressions
  #+cljs (:require [goog.string :as gstring]))

(defn my-trim [s]
  #+clj (.. s toString trim)
  #+cljs (gstring/trim s))

(my-trim " Hello CL? ")

The patches add a dynamic variable called *features* to the clojure.core and cljs.core namespaces, that should contain the supported features of the platform in question as keywords. Unlike in Common Lisp, the variable is a Clojure set and not a list. In Clojure the set contains the :clj keyword, and in ClojureScript the :cljs keyword.

I would like to get feedback on the following issues: 

  • Are those keywords ok? Is :jvm for Clojure and :js for ClojureScript better? 
    • Use "clj" and "cljs" (examples above modified)
  • Should ClojureScript add something like :rhino, :v8 or :browser as well?
    • No, would prefer not to
  • Someone mentioned that this should support Clojure's namespaces. How should this be done?
    • not sure what this means
  • How should the compilation process work with this extension? 
    • The reader in Clojure and ClojureScript would understand the expressions and read (or not read) the appropriate parts. The compiler works the same after reading.
  • Will the ClojureScript compiler read *.clj files?  What happens to *.cljs files?
    • No changes in this.

To run the ClojureScript tests, drop a JAR named "clojure.jar" that has the Clojure patch applied into ClojureScript's lib directory.

References

The Common Lisp Hyperspec about the Sharp Sign macros:

Examples of Common Lisp's Feature Expresions:

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:

Labels: