Skip to content

Commit

Permalink
refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
miner committed Mar 9, 2014
1 parent 0d9727a commit 65760f1
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 127 deletions.
131 changes: 91 additions & 40 deletions README.md
@@ -1,62 +1,95 @@
# wilkins

Experimental lib for Clojure conditional feature reader using tagged literals
Experimental lib for Clojure conditional features.

## Usage

leiningen `[com.velisco/wilkins "0.1.8"]`
leiningen dependency `[com.velisco/wilkins "a.b.c"]`

In case I forget to update the version number here in the README, the latest version is available on
Clojars.org:
where "a.b.c" stands for the version number shown below (from Clojars.org)

[![Wilkins on clojars.org][latest]][clojar]

[latest]: https://clojars.org/com.velisco/wilkins/latest-version.svg "Wilkins on clojars.org"
[clojar]: https://clojars.org/com.velisco/wilkins

The main namespace is `miner.wilkins`.

## Example
There are two ways to use wilkins. First, there is a data-reader that implements
conditional compilation at read-time. Second, there is a macro that implements conditional
compilation at compile time. In both cases, feature expressions determine which form is
actually evaluated.

## Examples

In your `data_readers.clj` you can assign a tag to enable the conditional reader like this:

{feature/condf miner.wilkins/condf}
{feature/condf miner.wilkins/condf-reader}

Then in your source:

(require 'miner.wilkins)
(require '[miner.wilkins :as w])

(println #feature/condf [(and jdk-1.6+ clj-1.5.*) "Ready for reducers" else "No reducers for you."])
#feature/condf [
(and jdk-1.6+ clj-1.5.*) (call-my-fast-reducer-code)
else (some-old-fashioned-code) ]

Note that all the clause are contained in a vector literal so the data reader applies to the
whole vector as its single argument.
whole vector as its single argument. The pairs of forms are like a `cond` but the test is a
feature requirement expression. The data-reader will return the single form following the first
matching feature requirement. That is the only form that will be seen by the compiler.

The macro `compile-condf` is similar but doesn't have require the vector notation around the
clauses. It reads more like a `cond` but the feature expressions are evaluated at
compile-time.

(w/compile-conf
(and jdk-1.6+ clj-1.5.*) (call-my-fast-reducer-code)
else (some-old-fashioned-code))

If you want to evaluate feature expressions at runtime, use `feature?`.

(w/feature? 'clj-1.5+)

## Conditional Feature Reader

`condf` is short for conditional feature reader. The `condf` tag prefaces a sequence of clauses
`condf` is short for "conditional feature". The `condf` tag marks a sequence of clauses
similar to `cond`, but each test is a feature requirement. The first feature requirement to be
satisfied causes the reader to return the following expression. As a special case, the symbol
`else` always qualifies. If no feature requirement is satisfied, the reader effectively returns
nil.

The feature requirement specifies a feature name and an optional version. The feature
requirements are evaluated by the `condf` data-reader at read-time so the compiler never sees
unsuccessful clauses.
requirements are evaluated by the `condf-reader` data-reader at read-time so the compiler
never sees unsuccessful clauses. The `compile-condf` macro is similar but all its forms are
read by the reader so it's slightly less flexible with expressions that might not be legal
in certain Clojure variants. Note: We intend to support Clojurescript, but that is not currently
implemented.

## Feature Notation

A canonical "feature requirement" is defined by an identifier (symbol) and version
information in a map following the same scheme as `*clojure-version*`. We'll give the
details below, but typically we use a compact notation for versions and feature requirements.

The compact form of a feature requirement uses a single symbol. For example, `clj-1.4`
means Clojure 1.4. The alphabetic part is the feature name, and the dotted number part is
the version number, separated by a hyphen (-). A trailing `+` means "or greater". A
means Clojure 1.4. The alphabetic part is the feature identifier and the dotted number part
is the version number, separated by a hyphen (-). A trailing `+` means "or greater". A
trailing `.*` means "any increment", but the previous parts must match exactly. Only one
`+` or `*` is allowed in a feature requirement. A qualifier string may follow the version
number (separated by a hyphen), but in that case an exact match is required. For example,
`clj-1.5-RC1` matches exactly Clojure "1.5.0-RC1", and not any other version. A hyphen is
required to separate the feature name from version, but the version information is
optional. Also, hyphens may be used in the feature name as well. However, if the feature
name itself contains a hyphen followed by digit, the parser will be confused. In that case, you
can either quote the symbol or use the vector form of the feature requirement.
required to separate the feature name from version, but the version information is optional.
Also, hyphens may appear in the feature name. In the rare case in which your feature name
itself actually contains a hyphen followed by digit, you can either quote the symbol or use
the vector form of the feature requirement to avoid parsing confusion.

A feature requirement can also be a literal vector of a name symbol and a version string. This allows you to use
a feature name that contains a digit, such as `[lucky-7 "1.2+"]`. The version string can be
omitted if you don't care about versioning.
The vector form of a feature requirement contains an identifier symbol (not subject to
version parsing) and a version string. This allows you to use a feature name that contains
a digit, such as `[lucky-7 "1.2+"]`. The version string can be omitted if you don't care
about versioning.

## Feature Declaration

A feature declaration is simply a Clojure var with metadata declared for the `:feature`
key.
Expand All @@ -66,7 +99,7 @@ key.

The `version` macro expands a version string into a map form, similar to the style of
`*clojure-version*`, with the keys `:major`, `:minor`, `:incremental` and `:qualifier`. A
map-entry may be omitted if the value is nil.
map-entry may be omitted if the value is nil. No wildcards are allowed when declaring a version.

A feature requirement without a namespace implicitly refers to the current namespace
(`*ns*`) or the `miner.wilkins.features` namespace which has a few predefined features. For
Expand All @@ -79,9 +112,8 @@ require that a Java class exists by using its name as the feature symbol (withou
version). As in Clojure, a Java class is named by a simple symbol (no namespace) containing
internal periods -- for example, `java.util.concurrent.ForkJoinPool`.

Boolean combinations use list expressions beginning with `and`, `or` or `not`. For example: `(and
jdk-1.6+ clj-1.5+)`.

Boolean combinations are encoded as list expressions beginning with `and`, `or` or `not`.
For example: `(and jdk-1.6+ clj-1.5+)`.

## Summary

Expand All @@ -103,27 +135,46 @@ Here are a few examples to show the how the parsing works for the compact symbol
* `'lucky-7` (quoted) expands into `{:feature lucky-7 :major :*}`
* `[lucky-7]` (vector without version) also expands into `{:feature lucky-7 :major :*}`

## Canonical Feature Requirement

The canonical feature requirement is a map similar to `*clojure-version*`.

The "version" part has possible keys (with value types): `:major` (int), `:minor` (int),
`:incremental` (int), and `:qualifier` (string). For informational purposes it might also
contain `:version` (string).

The feature requirement has the key `:feature` with a symbol as the value, and the version
keys described above. It can also have the key `:plus` (boolean). The least significant of
the `:major`, `:minor`, or `:incremental` values may be `:*` (a keyword) in addition to the
usual int value. The `:*` matches any value. For example, 1.4.* matches any version in the
1.4 series (1.4.0, 1.4.1, etc.) but not 1.5. If the version doesn't matter, then :major
should be given as `:*`. (See `as-feature-request`.) When the `:plus` value is true, then
the version matches exactly the given :major, :minor, and :incremental keys or anything
greater. For example, 1.4+ matches 1.4.1, 1.5.0, etc. If a `:qualifier` (string) is
specified as a requirement, then the match must be exact, and wildcards are not allowed.


## Runtime

The `feature-cond` macro is like `cond` but the tests are feature specifications (unquoted as in the
`condf` data-reader). This macro, of course, performs the checks at runtime.
If you're using AOT ("ahead of time") compilation, you have to be careful that your actual
runtime corresponds to the compilation environment. If the conditional compilation depended
on JDK 1.7, but you later use that jar with JDK 1.6 you might have a problem. If you want
to do your feature tests at runtime (or in a REPL), use the `feature?` predicate and pass it
feature expression. (Note the you typically have to quote a symbolic requirement since this
is a normal function call, not a macro.)

(cond
(w/feature? '(and clj (not foo-2.4+))) (do-something 1 2)
(w/feature? '(or clj-1.5 [clj "1.4"])) :clj
:else :something-else)

(feature-cond
(and clj (not foo-2.4+)) (do-something 1 2)
(or clj-1.5 [clj "1.4"]) :clj
else :something-else)

For conditional code based on the particular version of the JDK or Clojure, it is usually
best to use the `feature-cond` macro rather than the data-reader `condf`. Be careful about
ahead-of-time compilation (AOT) with data-readers -- the compiled code will be conditioned
by the compile-time environment. It will not be "read" again when loaded from the jar into
the current runtime, which might have a different JDK or Clojure version.
## Clojurescript not implemented yet

On the other hand, `condf` is intended to be useful for conditional code across variants of Clojure
(such as ClojureScript) where the platform or host language may be so different that runtime checks
would not be feasible. We have not yet implemented the ClojureScript version of `condf` so that
needs a bit more work to validate the approach.
Wilkins is intended to be useful for conditional code across variants of Clojure (such as
ClojureScript) where the platform or host language may be so different that runtime checks
would not be feasible. We have not yet implemented the ClojureScript version of
`condf-reader` so that needs a bit more work to validate the approach.


## Bob Wilkins
Expand Down

0 comments on commit 65760f1

Please sign in to comment.