WISP
wisp is a homoiconic JavaScript dialect with Clojure syntax, s-expressions and macros. Unlike ClojureScript, wisp does not depend on the JVM and is completely self-hosted, embracing native JavaScript data structures for better interoperability.
The main goal of wisp is to provide a rich subset of Clojure(Script) so that packages written in wisp can work seamlessly with Clojure(Script) and JavaScript without data marshalling or code changes.
wisp also does its best to compile down to JavaScript you would have written by hand - think of wisp as markdown for JavaScript programming, but with the added subtlety of LISP S-expressions, homoiconicity and powerful macros that make it the easiest way to write JavaScript.
Try Wisp
You can try wisp on your browser by trying the interactive compiler (repo) or an online REPL with syntax highlighting.
Install
You can install wisp locally via npm
by doing:
npm install -g wisp
...and then running wisp
to get a REPL. To compile standalone .wisp
files, simply do:
cat in.wisp | wisp > out.js
Language Essentials
Data structures
nil
nil
is just like JavaScript undefined
with the difference that it
cannot be redefined. It compiles down to void(0)
in JavaScript.
nil ; => void(0)
Booleans
true
/ false
are directly equivalent to plain JavaScript booleans:
true ; => true
Numbers
wisp numbers are directly equivalent to JavaScript numbers:
1 ; => 1
Strings
wisp strings are JavaScript strings:
"Hello world"
...and can be multi-line:
"Hello,
My name is wisp!"
Characters
Characters are syntactic sugar for single character strings:
\a ; => "a"
\b ; => "b"
Keywords
Keywords are symbolic identifiers that evaluate to themselves:
:keyword ; => "keyword"
Since in JavaScript string constants fulfill the purpose of symbolic identifiers, keywords compile to equivalent strings in JavaScript. This allows using keywords in Clojure(Script) and JavaScript idiomatic fashion:
(window.addEventListener :load handler false)
Keywords can also be invoked as functions, although that too is syntax sugar that compiles to property access in JavaScript:
(:bar foo) ; => (foo || 0)["bar"]
Note that keywords in wisp are not real functions so they can't be composed or passed to high order functions.
Vectors
wisp vectors are plain JavaScript arrays, but nevertheless all standard library functions are non-destructive and pure functional as in Clojure.
[ 1 2 3 4 ]
Note: Commas are considered whitespace and can be used if desired:
[1, 2, 3, 4]
Dictionaries
wisp does not have Clojure-like value-to-value maps by default, but rather dictionaries that map to plain JavaScript objects.
Therefore, unlike Clojure, keys cannot consist of arbitrary types.
{ "foo" bar :beep-bop "bop" 1 2 }
Like with vectors, commas are optional but can come handy for separating key value pairs.
{ :a 1, :b 2 }
Lists
What would be a LISP without lists? wisp being homoiconic, its code is made up of lists representing expressions.
As in other LISPs, the first item of an expression is an operator or function that takes the remainder of the list as arguments, and compiles accordingly to JavaScript:
(foo bar baz) ; => foo(bar, baz);
The compiled JavaScript is quite unlikely to end up with lists as they primarily serve their purpose at compile time. Nevertheless lists are supported and can be used (more further down)
Arrays
wisp partially emulates Clojure(Script) handling of arrays in two ways:
- By using
get
, which compiles to guarded access in JavaScript:
(get [1 2 3] 1) ; => ([1, 2, 3] || 0)[0]
- By using
aget
, which compiles to unguarded access and can (for the moment) also be used to perform item assignments:
(aget an-array 2) ; => anArray[2];
(set! (aget an-array 2) "bar") ; => anArray[2] = "bar";
(aset
will be added ASAP for symmetry, but you can easily define an equivalent macro for the moment)
Conventions
wisp tries very hard to compile to JavaScript that feels hand-crafted while trying to embrace LISP-style idioms and naming conventions, and translates them to equivalent JavaScript conventions:
(dash-delimited) ; => dashDelimited
(predicate?) ; => isPredicate
(**privates**) ; => __privates__
(list->vector) ; => listToVector
This makes for very natural-looking code, but also allows some things to be expressed in different ways. For instance, the following function invocations will translate to the same things:
(parse-int x)
(parseInt x)
(array? x)
(isArray x)
Special forms
There are some special operators in wisp in the sense that they compile to JavaScript expressions rather then function calls.
Identically-named functions are also available in the standard library to allow function composition.
Arithmetic operations
wisp comes with special forms for common arithmetic:
(+ a b) ; => a + b
(+ a b c) ; => a + b + c
(- a b) ; => a - b
(* a b c) ; => a * b * c
(/ a b) ; => a / b
(mod a b) ; => a % 2
Comparison operations
...and special forms for common comparisons:
(identical? a b) ; => a === b
(identical? a b c) ; => a === b && b === c
(= a b) ; => a == b
(= a b c) ; => a == b && b == c
(> a b) ; => a > b
(>= a b) ; => a >= b
(< a b c) ; => a < b && b < c
(<= a b c) ; => a <= b && b <= c
Logical operations
...and special forms for logical operations:
(and a b) ; => a && b
(and a b c) ; => a && b && c
(or a b) ; => a || b
(and (or a b)
(and c d)) ; (a || b) && (c && d)
Definitions
Variable definitions also happen through special forms:
(def a) ; => var a = void(0);
(def b 2) ; => var b = 2;
Assignments
In wisp variables can be set to new values via the set!
special form.
Note that in functional programing binding changes are a bad practice (avoiding these will improve the quality and testability of your code), but there are always cases where this is required for JavaScript interoperability:
(set! a 1) ; => a = 1
The !
suffix is a useful visual reminder that you're causing a side-effect.
Conditionals
Conditional code branching in wisp is expressed via the ìf
special form.
As usual, the first expression following if
is a condition - if it evaluates to true
the result of the if
form will be the second expression, otherwise it'll be the third "else" expression:
(if (< number 10)
"Digit"
"Number")
The third ("else") expression is optional, and if missing and the conditional evaluates to true
the result will be nil
.
(if (monday? today) "How was your weekend")
Combining expressions
In wisp everything is an expression, but sometimes one might want to combine multiple expressions into one, usually for the purpose of evaluating expressions that have side-effects. That's where do
comes in:
(do
(console.log "Computing sum of a & b")
(+ a b))
do
can take any number of expressions (including 0
, in which case it will evaluate to nil
):
(do) ; => nil
Bindings
The let
special form evaluates sub-expressions in a lexical context in which symbols in its binding-forms (first item) are bound to their respective expression results:
(let [a 1
b (+ a 1)]
(+ a b))
; => 3
Functions
wisp functions are plain JavaScript functions
(fn [x] (+ x 1)) ; => function(x) { return x + 1; }
wisp functions can have names, just as in JavaScript
(fn increment [x] (+ x 1)) ; => function increment(x) { return x + 1; }
wisp function declarations can also contain documentation and some metadata:
(defn sum
"Return the sum of all arguments"
{:version "1.0"}
[x] (+ x 1))
Function _expressions, though, can only have names:
(fn increment
{:added "1.0"}
[x] (+ x 1))
Note: Docstrings and metadata are not included in compiled JavaScript yet, but support for that is planned.
Arguments
wisp makes capturing of remaining (rest
) arguments a lot easier than JavaScript. An argument that follows an ampersand (&
) symbol will capture the remaining args in a standard vector (i.e., array).
(fn [x & rest]
(rest.reduce (fn [sum x] (+ sum x)) x))
Overloading Functions
In wisp functions can be overloaded depending on arity (the number of arguments they take), without introspection of remaining arguments.
(fn sum
"Return the sum of all arguments"
{:version "1.0"}
([] 0)
([x] x)
([x y] (+ x y))
([x & more] (more.reduce (fn [x y] (+ x y)) x)))
If a function does not have variadic overload and more arguments are passed to it, it throws an exception.
(fn
([x] x)
([x y] (- x y)))
Loops and TCO
A classic way to build a loop in LISP is via recursion, wisp provides a loop
recur
construct that allows for tail call optimization:
(loop [x 10]
(if (> x 1)
(print x)
(recur (- x 2))))
Other Special Forms
Instantiation
In wisp type instantiation has a concise form, by way of suffixing the function with a period (.
):
(Type. options)
However, the more verbose but more JavaScript-like form is also valid:
(new Class options)
Method calls
In wisp method calls are no different from function calls, but prefixed with a period (.
):
(.log console "hello wisp")
...and, of course, the more JavaScript-like forms are supported too:
(window.addEventListener "load" handler false)
Attribute access
In wisp, attribute access is also treated like a function call, but attributes need to be prefixed with .-
:
(.-location window)
Compound properties can be accessed via the get
special form:
(get templates (.-id element))
Catching Exceptions
In wisp exceptions can be handled via the try
special form. As with everything
else, the try
form is also an expression that evaluates to nil
if no handling
takes place.
(try (raise exception))
...the catch
form can be used to handle exceptions...
(try
(raise exception)
(catch error (.log console error)))
...and the finally
clause can be used too:
(try
(raise exception)
(catch error (recover error))
(finally (.log console "That was a close one!")))
Throwing Exceptions
In a non-idiomatic twist (but largely for symmetry and JavaScript interop), the throw
special form allows throwing exceptions:
(fn raise [message] (throw (Error. message)))
Macros
wisp has a powerful programmatic macro system which allows the compiler to be extended by user code.
Many core constructs of wisp are in fact normal macros, and you are encouraged to study the source to learn how to build your own. Nevertheless, the following sections are a quick primer on macros.
quote
Before diving into macros too much, we need to learn a few more things. In LISP any expression can be quoted to prevent it from being evaluated.
As an example, take the symbol foo
- by default, you will be
evaluating the reference to its corresponding value:
foo
But if you wish to refer to the literal symbol, this is how you do it:
(quote foo)
or, as shorthand:
'foo
Any expression can be quoted to prevent its evaluation (these are not, however, compiled to JavaScript):
'foo
':bar
'(a b)
An Example Macro
wisp doesn't have the unless
special form or a macro, but it's trivial
to implement it via macros.
But it's useful to try implementing it as a function to understand a use case for macros, so let's get started:
unless
is easy to understand -- we want to execute a body
unless a given condition
is true
:
(defn unless-fn [condition body]
(if condition nil body))
But since function arguments are evaluated before the function itself is called, the following code will always write a log message:
(unless-fn true (console.log "should not print"))
Macros solve this problem, because they do not evaluate their arguments immediately. Instead, you get to choose when (and if!) the arguments to a macro are evaluated. Macros take items of the expression as arguments and return a new form that is compiled instead.
(defmacro unless
[condition form]
(list 'if condition nil form))
The body of the unless
macro executes at macro expansion time, producing an if
form for compilation. This way the compiled JavaScript is a conditional instead of a function call.
(unless true (console.log "should not print"))
syntax-quote
Simple macros like the above could be written via templating and expressed as syntax-quoted forms.
syntax-quote
is almost the same as plain quote
, but it allows
sub expressions to be unquoted so that form acts as a template.
The symbols inside the form are resolved to help prevent inadvertent symbol capture, which can be done via unquote
and unquote-splicing
forms.
(syntax-quote (foo (unquote bar)))
(syntax-quote (foo (unquote bar) (unquote-splicing bazs)))
Note that there is special syntactic sugar for both unquoting operators:
- Syntax quote: Quote the form, but allow internal unquoting so that the form acts as template. Symbols inside the form are resolved to help prevent inadvertent symbol capture.
`(foo bar)
- Unquote: Use inside a syntax-quote to substitute an unquoted value.
`(foo ~bar)
- Splicing unquote: Use inside a syntax-quote to splice an unquoted list into a template.
`(foo ~bar ~@bazs)
For example, the built-in defn
macro can be defined with a simple
template macro. That's more or less how the built-in defn
macro is implemented.
(defmacro define-fn
[name & body]
`(def ~name (fn ~@body)))
Now if we use define-fn
form above, the defined macro will be expanded
at compile time, resulting into different program output.
(define-fn print
[message]
(.log console message))
Not all of the macros can be expressed via templating, but all of the language is available to assemble macro expanded forms.
Another Macro Example
As an example, let's define a macro to ease functional chaining, a technique popular in JavaScript but usually expressed via method chaining. A typical use of that would be something like:
open(target, "keypress").
filter(isEnterKey).
map(getInputText).
reduce(render)
Unfortunately, though, it usually requires that all the chained functions need to be methods of an object, which is very limited and has the undesirable effect of making third party functions "second class".
But using macros we can achieve similar chaining without such tradeoffs, and chain any function:
(defmacro ->
[& operations]
(reduce
(fn [form operation]
(cons (first operation)
(cons form (rest operation))))
(first operations)
(rest operations)))
(->
(open target :keypress)
(filter enter-key?)
(map get-input-text)
(reduce render))
Import/Export (Symbols and Modules)
Exporting Symbols
All the top level definitions in a file are exported by default:
(def foo bar)
(defn greet [name] (str "hello " name))
...but it's still possible to define top level bindings without exporting them via ^:private
metadata:
(def ^:private foo bar)
...and a little syntax sugar for functions:
(defn- greet [name] (str "hello " name))
Importing
Module importing is done via an ns
special form that is manually
named. Unlike ns
in Clojure(Script), wisp takes a minimalistic
approach and supports only one essential way of importing modules:
(ns interactivate.core.main
"interactive code editing"
(:require [interactivate.host :refer [start-host!]]
[fs]
[wisp.backend.javascript.writer :as writer]
[wisp.sequence
:refer [first rest]
:rename {first car rest cdr}]))
Let's go through the above example to get a complete picture regarding how modules can be imported:
-
The first parameter
interactivate.core.main
is a name of the module / namespace. In this case it represents module./core/main
under the packageinteractivate
. While this is not enforced in any way the common convention is that these mirror the filesystem hierarchy. -
The second string parameter is just a description of the module and is completely optional.
-
The
(:require ...)
form defines dependencies that will be imported at runtime, and the example above imports multiple modules: -
First it imports the
start-host!
function from theinteractivate.host
module. That will be loaded from the../host
location, since because module paths are resolved relative to a name, but only if they share the same root. -
The second form imports
fs
module and makes it available under the same name. Note that in this case it could have been written without wrapping it in brackets. -
The third form imports
wisp.backend.javascript.writer
module fromwisp/backend/javascript/writer
and makes it available via the namewriter
. -
The last and most complex form imports
first
andrest
functions from thewisp.sequence
module, although it also renames them and there for makes available under differentcar
andcdr
names.
While Clojure has many other kinds of reference forms they are not recognized by wisp and will therefore be ignored.
Types and Protocols
In wisp protocols can be defined same as in Clojure(Script), via defprotocol:
(defprotocol ISeq
(-first [coll])
(-rest [coll]))
(defprotocol ICounted
(^number count [coll] "constant time count"))
Above code will define ISeq
, ICounted
protocols (objects representing
those protocol) and _first
, _rest
, count
functions, that dispatch on
first argument (that must implement associated protocol).
Existing types / classes (defined either in wisp or JS) can be extended to implement specific protocol using extend-type:
(extend-type Array
ICounted
(count [array] (.-length array))
ISeq
(-first [array] (aget array 0))
(-rest [array] (.slice array 1)))
Once type / class implemnets some protocol, it's functions can be used on the instances of that type / class.
(count []) ;; => 0
(count [1 2]) ;; => 2
(-first [1 2 3]) ;; => 1
(-rest [1 2 3]) ;; => [2 3]
In wisp value can be checked to satisfy given protocol same as in Clojure(Script) via satisfies?:
(satisfies? ICounted [1 2])
(satisfies? ISeq [])
New types (that translate to JS classes) can be defined same as in Clojure(Script) via deftype form:
(deftype List [head tail size]
ICounted
(count [_] size)
ISeq
(-first [_] head)
(-rest [_] tail)
Object
(toString [self] (str "(" (join " " self) ")")))
Note: Protocol functions are defined as methods with unique names
(that include namespace info where protocol was defined, protocol
name & method name) to avoid name collisions on types / classes
implementing them. This implies that such methods aren't very
useful from JS side. Special Object
protocol can be used to
define methods who's names will be kept as is, which can be used
to define interface to be used from JS side (like toString
method above).
In wisp multiple types can be extended to implement a specific protocol using extend-protocol form same as in Clojure(Script) too.