Skip to end of metadata
Go to start of metadata

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 caseExample (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:

WhereItemDescriptionExample
allWell-known platform feature namesEach platform has a well-known platform feature when loading or compiling source files.clj, cljs, cljr
allReader 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]
#?(:clj (.. s toString trim)
:cljs (gstring/trim s)))
allReader syntax for conditional spliced expressionsread-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
#?@(:clj [(:require clojure.string)
(:import java.util.Date)]))
allReader 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
{:read-cond :allow}

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 
{:read-cond :allow}
"#?(:clj "clj" :default nil)") 
allPreserve 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
{:read-cond :preserve}
"#?(:clj foo :cljs bar)")
allRead-conditional data instanceIn preserve mode, this instance may be returned. See below for details on what it supports.
;; Construct like:
(reader-conditional  )
allType-independent tagged literal data instanceIn preserve mode, this instance may be returned. See below for details on what it supports.
;; Construct like:
(tagged-literal "js" {}) 
allNew .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: cljcljs, 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:

KeyValuesDescription
:eofvalue or :eofthrowIf 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
:featuresset of keyword featuresThis 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.

 

A similar approach can then be taken for reader conditionals when read in preserve-read-cond mode. 
Given #?(...) 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.

 
More background on this can be found in earlier work on the Feature Expressions page.

Patches

JIRA Tickets and patches supporting this proposal can be found in several tickets:

Building Portable Libraries

This section discusses different techniques for building portable libraries.
 
Case 1: Independent CLJS and CLJ files packaged in the same jar
 
Some libraries may provide independent implementations of a library for both CLJ and CLJS and be packaged in the same jar. Each platform will load the platform-specific resource:

 

 

Case 2: Mixed portable and non-portable CLJS and CLJ packaged in the same jar

The myproj/mylib.cljc file will contain reader conditional specializing it for multiple supported platforms. The jar might contain additional code specific only to certain platforms, which might be loaded by myproj.mylib for example.
 

 

 

Case 3: Library designed for open extension
 
A library designed for open extension isolates platform-specific code in one or more namespaces. The library code may compile against (but not include) a stub namespace or use a .cljc portable file to contain a default library implementation. The non-isolated code should all be .cljc files that are portable across platforms.

 

A separate jar would supply the platform-specific version of the ns:

 

 

Other additional jars would be used to supply other platform-specific versions of the ns. A consumer would use the first jar + the selected extension jar.

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. 


Labels: