Skip to content
/ wisp Public
forked from wisp-lang/wisp

Homoiconic JS with clojure syntax, s-expressions & macros

License

Notifications You must be signed in to change notification settings

robjens/wisp

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WISP

Build Status NPM version Dependency Status Gitter chat

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.

meta

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:

  1. By using get, which compiles to guarded access in JavaScript:
(get [1 2 3] 1) ; => ([1, 2, 3] || 0)[0]
  1. 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:

  1. 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)
  1. Unquote: Use inside a syntax-quote to substitute an unquoted value.
`(foo ~bar)
  1. 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:

  1. The first parameter interactivate.core.main is a name of the module / namespace. In this case it represents module ./core/main under the package interactivate. While this is not enforced in any way the common convention is that these mirror the filesystem hierarchy.

  2. The second string parameter is just a description of the module and is completely optional.

  3. The (:require ...) form defines dependencies that will be imported at runtime, and the example above imports multiple modules:

  4. First it imports the start-host! function from the interactivate.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.

  5. 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.

  6. The third form imports wisp.backend.javascript.writer module from wisp/backend/javascript/writer and makes it available via the name writer.

  7. The last and most complex form imports first and rest functions from the wisp.sequence module, although it also renames them and there for makes available under different car and cdr 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.

About

Homoiconic JS with clojure syntax, s-expressions & macros

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • wisp 99.0%
  • Other 1.0%