Problem
There are Clojure dialects (Clojure, ClojureScript, ClojureCLR) hosted on several different platforms. We wish to write libraries that can share as much portable code as possible while leaving flexibility to provide platform-specific bits as needed, then have this code run on all of them.
Use cases for platform-specific functionality currently handled by cljx:
Use case | Example (cljx) | Example (reader conditional) |
---|---|---|
Platform-specific require/import | In .cljx file which is preprocessed into .clj and .cljs files: | In .cljc file loadable in both Clojure and ClojureScript: |
Exception handling, catching "all" | In .cljx file which is preprocessed into .clj and .cljs files: | In .cljc file loadable in both Clojure and ClojureScript: |
Platform-specific calls for "hosty" library things (strings, math, dates, random numbers, uris, reflection warnings) | In .cljx file which is preprocessed into .clj and .cljs files: | In .cljc file loadable in both Clojure and ClojureScript: |
Protocol invocation (CLJS internal protocol names start with -, CLJ interfaces do not) | In .cljx file which is preprocessed into .clj and .cljs files: (See "-deref" in CLJS vs "deref" in CLJ.) | In .cljc file loadable in both Clojure and ClojureScript: |
Extending protocols to implementation-specific classes | In .cljx file which is preprocessed into .clj and .cljs files: | In .cljc file loadable in both Clojure and ClojureScript: |
Proposal summary
The feature expression proposal includes the following items:
Where | Item | Description | Example |
---|---|---|---|
all | Well-known platform feature names | Each platform has a well-known platform feature when loading or compiling source files. | clj , cljs , cljr |
all | Reader syntax for conditional expressions | read-cond or #?(feature expr ...) - alternating feature and expression (similar to cond). First feature that is included in the current feature set will result in the expr being read. | (defn my-trim [s] |
all | Reader syntax for conditional spliced expressions | read-cond-splicing or #?@(feature expr ...) - same as read-cond, but expression must be a java.util.List and will be spliced into the resulting read. | (ns a.b |
all | Reader syntax for conditional read :default | If no feature is matched in #? or #?@, :default is a well-known keyword indicating a default expression to use instead. If no expression is chosen, nothing (not nil, but literally nothing) is read. | #?(:clj (Foo.) :default []) |
all | Allow reader conditional read mode with reader option | In allow mode, reader conditional syntax is allowed. Reader conditionals are evaluated and replaced with the result of the appropriate branch. Reading source files containing reader conditionals without this option results in a runtime exception. | (read-string |
all | Preserve reader conditional read mode with reader option{:read-cond :preserve } | In preserve mode, the reader-conditional is not replaced by the selected branch but instead returned as a form containing all branches. Tagged literals and nested reader-conditionals in all non-chosen branches will be represented in data form. | (read-string |
all | Read-conditional data instance | In preserve mode, this instance may be returned. See below for details on what it supports. | ;; Construct like: |
all | Type-independent tagged literal data instance | In preserve mode, this instance may be returned. See below for details on what it supports. | ;; Construct like: |
all | New .cljc extension indicating a portable source file | Clojure, ClojureScript, and ClojureCLR should load both .cljc (which may have reader conditionals) and their own platform-specific files as source files. For .cljc files: the reader will be invoked with read-cond mode :allow. For .clj/.cljs files: the reader will be invoked with read-cond disabled. All other read modes (such as the repl) will disable reader conditionals by default. The reader may be invoked with appropriate options to enable this mode. |
Detailed Proposal
Feature Sets
The platform feature will be one of: clj
, cljs
, or cljr
. The platform feature will always be available when reading. The features default
, else
, and none
are reserved.
See "Reader options" below for more options on modifying the feature set for read.
Reader syntax
There are two new reader literal forms: #? (read-cond) and #?@ (read-cond-splicing). Both have the same syntax but the first reads as a single expression whereas the second is read as a list that will be spliced into the parent expression.
The syntax of the body of these forms consists of pairs of feature-condition and expression. The form is interpreted in a manner similar to cond. Feature conditions are tested in order until there is a match, then the corresponding expression is returned (and spliced if using #?@).
The well-known feature-condition :default
can be used to indicate a condition that always matches.
If no feature-conditions match and there is no :default, then the reader will read nothing (not nil, but literally no value will be returned) from the read-cond or read-cond-splicing forms.
Reader options and preserve-read-cond mode
The reader (via both read and read-string) can now be invoked with a map of options. Available options include:
Key | Values | Description |
---|---|---|
:eof | value or :eofthrow | If EOF is reached then return value, unless :eofthrow then throw runtime exception. |
:read-cond | :allow, :preserve | :allow = read conditionally. :preserve = read conditionally, and preserve all branches not taken as data |
:features | set of keyword features | This can be used to conditionally read forms with broader feature sets. The platform feature (:clj) will always be present. |
Example of direct use:
To read and preserve all read-conditional branches, use preserve mode to return a read-conditional instance. Within each un-chosen branch of the read-conditional, tagged literals will be represented in a data-only form (both of these are described in the next section). Example:
Read-conditional and tagged literal instances
A tagged literal read in an unchosen branch of a reader conditional with preserved-read-cond mode is read as a data-only tagged literal.
Given #some-tag some-form
read as x:
That is, the instance returned will be a clojure.lang.TaggedLiteral with a constructor function, keyword predicate lookup (for :tag and :form), and print capability.
#?(...)
read as x:
That is, the instance returned will be a clojure.lang.ReadConditional with a constructor function, keyword predicate lookup (for :form and :splicing), and print capability.
New portable file extension: .cljc
To create a single portable file we need a file that can be found and loaded in both Clojure and ClojureScript. Existing file extensions of .clj and .cljs must be read in their respective platforms for backwards compatibility. ClojureScript currently uses .clj files for macro definition and they may live in the same directory as .cljs source files. Thus, mixing .clj and .cljs files in the same ClojureScript source tree is potentially problematic for some code bases.
Because of all this, the proposal adds a new file extension for Clojure files designed to be portable: .cljc. These files are the only place a reader conditional is allowed. Both Clojure and ClojureScript should load these files as source and apply reader conditionals during read.
When finding a library in any Clojure dialect, the platform-specific resource will be found and loaded first (.clj or .cljs) before the portable file (.cljc) is found or loaded. This allows the opportunity to override a platform-agnostic file with a platform-specific implementation of a namespace.
Patches
JIRA Tickets and patches supporting this proposal can be found in several tickets:
- http://dev.clojure.org/jira/browse/CLJ-1424 - Clojure
- http://dev.clojure.org/jira/browse/TRDR-14 - tools.reader, used by ClojureScript
- http://dev.clojure.org/jira/browse/CLJS-27 - ClojureScript
- http://dev.clojure.org/jira/browse/TNS-34 - tools.namespace
Building Portable Libraries
A separate jar would supply the platform-specific version of the ns:
Future Extensions
Open feature set
While the reader can be invoked now with an open feature set, that is not available in general cljc files supporting reader conditionals. In the future open features may be allowed.
Default data reader for tagged reader instances
Currently, if no data reader is found for a tagged literal then the default-data-reader-fn will be invoked (if something has been installed for that fn). By default, no function is installed.
The new tagged literal constructor could be the default data reader fn. In that case, an unknown tagged literal would result in reading the tagged literal in a form that does not require a known type and where the tagged literal data can be recovered and/or passed along to another system.
Boolean feature combinations
The proposal above does not include support for boolean feature combinations (and, or, not) but it would be straightforward to extend the reader-conditional syntax to support it using something like this:
#?((and :clj :arch/osx) 1 :default 2)
Open extensions
Instead of default, a reader conditional could end with a final solitary (namespaced!) keyword serving as a semantic label for extension:
#?(:clj this :cljs that ::whatever-extension)
Given this, some read-time mechanism could be consulted to see if there is an entry for :your-lib/whatever-extension. If so, it is used as the value read from the form, else the form logic runs. Then if none of the conditions hold and no default it is an error, since there is a way to provide customization for your environment w/o changing the source.
This would also allow for shared, named snippets:
#?(:clojure.port/date-ctor)
There is a lot more infrastructure required for this extensibility, and it remains a future prospect.