Operators

undefined?, null?, true?, false?, zero?, boolean?, number?,
string?, object?, array?, function?,
=, !=, !, >, <, <=, >=, +, -, *, /, %, &&, ||.

Note: = and != work like === and !== in Javascript.

LispyScript Statements

(-> (function expression) (.key1 args1 ...) (.key2 args2 ...) ...)

"->" is the method chaining macro.

Consider the JavaScript below.

$("#xyz").required().alphanum().min(3).max(30).with("email");

The same JavaScript in LispyScript using the method chaining macro.

(-> ($ "#xyz") (.required) (.alphanum) (.min 3) (.max 30) (.with "email"))

The node server example using the method chaining macro.

(->
  (require "http")
  (.createServer
    (function (request response)
      (response.writeHead 200 {'Content-Type': 'text/plain'})
      (response.end "Hello World\n")))
  (.listen 3000 "127.0.0.1"))

(var name1 value1 name2 value2 ...)

var allows you to create lexically scoped variables in the current scope. This will compile to the JavaScript var statement so the variables will be hoisted.

(var a 1 b 2)    =>  var a = 1,
                         b = 2;

(str (string expression) ...)

Adds up all the strings.

(var title "My Home Page")
(console.log
  (str "<!DOCTYPE html>
<html>
<head>
  <title>" title "</title>
</head>
<body class=\"test\">
Hello World
</body>
</html>"))

In LispyScript double quoted strings are multiline strings. As you can see above they span multiple lines. If you need a double quote inside the string, escape it with \".

(array elem1 elem2 ....)

Create a Javascript Array.

(array 1 2 3 4)

You can also create an array using the Javascript literal notation like [1, 2, 3, 4] however there is a difference. The literal notation is evaluated in the Javascript context, and will work in most cases except when you want some LispyScript code evaluated, like when one of the elements is a LispyScript anonymous function. And unlike Javascript "new Array" a single integer passed will still create a proper array with the int as the first element.

(arrayInit length initialVal)

Create a Javascript Array of length length, with all elements initialized to initialVal

(arrayInit 5 null)  =>  [null, null, null, null, null]

(arrayInit2d outerLength innerLength initialVal)

Create a Javascript Array of length outerLength, with all elements initialized to arrays of length innerLength, and all elements of inner arrays initialized to initialVal

(arrayInit2d 2 3 null)  =>  [[null, null, null], [null, null, null]

(object key1 val1 key2 val2 ....)

Create a Javascript Object.

(object a 1 b 2 c 3) => {a: 1, b:2, c:3}

(new Classname arg1 arg2 ....)

Instantiate a new Javascript Object.

(new Date)
(new Date "October 13, 1975 11:13:00")

(if (condition) (if true expression) (if false expression))

If takes a conditional expression and evaluates the true expression if the condition is true, or the false expression otherwise.

(cond (condition1) (expression1) (condition2) (expression2) ... (optional default expression))

The cond statement (switch in Javascript) takes condition-expression pairs and an optional default expression. The default expression is nothing but the last condition set to true.

(cond
  (number? x) (console.log "numbers are ok")
  (string? x) (console.log "strings are ok")
  (boolean? x) (console.log "booleans are ok")
  true (console.log "This type is NOT ok"))

(when (condition) (expression1) (expression2) ...)

The when statement evaluates a set of expressions passed as it arguments when the condition is true.

(unless (condition) (expression1) (expression2) ...)

The unless statement evaluates a set of expressions passed as it arguments when the condition is false.

(do (expression1) (expression2) ...)

The do statement evaluates a set of expressions passed as it arguments.

(function (arguments expression) (expression1) (expression2) ... )

Creates an anonymous function.

(try (expression1) (expression2) ... (catch function))

Try takes a set of expressions and evaluates them. The last expression must be a function, that will be called in case an exception is thrown. The function is called with the error object.

(var fs (require 'fs'))
(var outfile "text.txt")
(try
  (fs.writeFileSync outfile "Hello World")
  (function (err)
    (console.log (+ "Cannot write file " outfile)
    (process.exit 1)))

(include "string filename")

Includes a file to be compiled with this compilation unit. It will also search the 'includes' folder for Lispyscript modules like 'html.ls'.

(throw (expression))

Will throw an error. (expression) can be string or object or an expression

Comments

Comments in LispyScript start with a ; and span the rest of the line.

Iteration & Looping

(loop (arguments) (initial values) (expression1) (expression2) ...)

The loop - recur construct is a tail call optimised looping expression. It creates a binding for a set of arguments with a set of initial values. A recusion point is set by using a recur expression which passes control back to the top of the loop with a new set of bindings for the arguments.

(loop (result x) ([] 5)
  (if (= 0 x)
    result
    (do
      (result.push x)
      (recur result --x))))

The example above will return a countdown array starting from 5 "[5, 4, 3, 2, 1]"

Read more about loop recur and tail call optimization here.

(each array iterator [(context)])

each takes an array, an iterator function and an optional context. The iterator is called for each element of the array with element value, index, array. If context is provided then "this" inside the iterator function will be the context object.

(each [1, 2, 3]
  (function (elem index list)
    (console.log elem)))

(each2d array iterator)

each2d takes an array whose elements are also arrays (2d array), and calls the iterator function for each element. The iterator is called with the following arguments. value innerIndex outerIndex innerArray outerArray.

(eachKey object (iterator) [context])

eachKey iterates over a map (object) and calls the iterator with value, key, object respectively.

(reduce array (iterator) [initial])

reduce takes an array, an iterator function and an optional initial value. The iterator is called with the following paramaters, previous value returned by the last call to iterator, current value of array element, array index, and array. First time around the initial value is used for previous value. If no initial value is given, the previous value is set to the first element of the array and the iteration starts from the second element. The last value returned by the iterator is returned by reduce.

(reduce [1,2,3,4]
  (function (accum val i list)
    (+ accum val))))

The above call to reduce will return 10.

(map array (iterator) [(context)])

map takes an array, an iterator function and an optional context. The iterator is called for each element of the array with element value, index, array. The returned value from the iterator for each call is pushed into a new array. map returns the new array. If context is provided then "this" inside the iterator function will be the context object.

(for (name1 array1 [name2 array2 ...] (result expression))

In LispyScript for is a list comprehension expression. for does not work like the JavaScript "for" statement.

(for
  (a [1,2,3] 
   b [3,4,5])
  (+ a b))

;; ==> [ 4, 5, 6, 5, 6, 7, 6, 7, 8 ]

;; we only want results that are <= 6 below

(for
  (a [1,2,3]
   b [3,4,5])
  (when (<= (+ a b) 6)
    (+ a b)))

;; ==> [ 4, 5, 6, 5, 6, 6 ]

(for 
  (letters ["a", "b", "c"]
   numbers [3, 4, 5])
  [letters, numbers])

;; ==> [['a',3],['a',4],['a',5],['b',3],['b',4],['b',5],['c',3],['c',4],['c',5]]

The bindings for a and b are made for every element of the corresponding arrays. In the second example we applied a when condition to the result expression to keep out results greater than six.

Macros

(macro name (arguments expression) (template expression))

LispyScript is not a dialect of Lisp. There is no list processing in LispyScript . LispyScript is Javascript using a Lispy syntax (a tree syntax). This is so that we can manipulate the syntax tree while compiling, in order to support macros.

You can define a macro.

(macro array? (obj)
  (= (Object.prototype.toString.call ~obj) "[object Array]"))

The 'array?' conditional is defined as a macro in LispyScript. The 'macro' expression takes a name as its second element, a parameters list in the third element, and the fourth element is the template to which the macro will expand.

Now let us create a Lisp like 'let' macro in LispyScript.

(macro let (names vals rest...)
  ((function ~names ~rest...) ~@vals))

(let (name email tel) ("John" "john@example.org" "555-555-5555")
  (console.log name)
  (console.log email)
  (console.log tel))

The 'let' macro creates lexically scoped variables with initial values. It does this by creating an anonymous function whose argument names are the required variable names, sets the variables to their initial values by calling the function immediately with the values. The macro also wraps the required code inside the function.

Now lets look at the call to the 'let' macro. 'names' will correspond to '(name email tel)'. 'rest...' corresponds to '(console.log name) (console.log email) (console.log tel)', which is the rest of the expressions after vals. We want to dereference these values in the macro template, and we do that with '~names', '~rest...'. However 'vals' corresponds to ("John" "john@example.org" "555-555-5555"). But thats not the way we want to dereference it. We need to dereference it without the parenthesis. For that we use '~@vals'.

We don't really need 'let' in LispyScript. We have 'var'. But if you need it, you can extend LispyScript by adding this macro to your code. Thats the power of macros. You can extend the language itself or create your own domain specific language.

Modules

You can write nodejs style modules in LispyScript and require them in LispyScript or JavaScript.

mymodule.ls
===========

(set module.exports
  (function (x) (* x x)))

main.js
=======

require("lispyscript/lib/require")
var square = require("./mymodule")
console.log(square(2))

Monads

Monads allow you to process data in steps. There are different types of monads, and each type provides you with additional processing rules for each step. Fortunately you don't have to understand the workings of monads to use them. LispyScript comes with four builtin monad types. The "identity monad", "maybe monad", "array monad" and "state monad". You can also write your own monads in LispyScript.

;; using identity monad

(doMonad identityMonad
  (a 1 
   b (* a 2))
  (+ a b))

The expression above will return number 3. In general the domonad expression is written like below.

(domonad name (varname1 expression1 varname2 expression2 ...) (result expression))

Note that the variable a in step 1 is visible in step 2, and both a and b are visible in the result expression.

;; using maybe monad

(doMonad maybeMonad
  (a (task1) 
   b (task2))
  (doSomething))

The maybe monad is called so because may be it will do something. The way it works is that if any of the steps returns null it will return null and not carry out the rest os the steps.

The array monad is a array comprehension.

;; using array monad

(doMonad arrayMonad
  (a [1,2,3] 
   b [3,4,5])
  (+ a b))

;; ==> [ 4, 5, 6, 5, 6, 7, 6, 7, 8 ]

;; we only want results that are <= 6 below

(doMonad arrayMonad
  (a [1,2,3]
   b [3,4,5])
  (when (<= (+ a b) 6)
    (+ a b)))

;; ==> [ 4, 5, 6, 5, 6, 6 ]

(doMonad arrayMonad 
  (letters ["a", "b", "c"]
   numbers [3, 4, 5])
  [letters, numbers])

;; ==> [['a',3],['a',4],['a',5],['b',3],['b',4],['b',5],['c',3],['c',4],['c',5]]

The bindings for a and b are made for every element of the corresponding arrays. In the second example we applied a when condition to the result expression to keep out results greater than six. This when condition can only be used with the array monad and maybe monad. Or to be more specific, monads that define mZero

The state monad allows you to pass the current state through a set of steps. The state could be anything, an integer, string, array, object etc. In the example below we will maintain state of an immutable stack through a set of steps.

;; First we write two functions push and pop,
;; which will be the operations we carry out.

;; push is a monadic function that accepts an element and returns a function.
;; The returned function accepts the current state (stack array),
;; creates a new state by creating a new array with the element
;; and concats the previous state to it. (we want an immutable state)
;; and returns a monadic value which is and array of two elements. The result
;; of the current operation (undefined) in this case and the new state.

(var push
  (function (element)
    (function (state)
      (var newstate [element])
      (array undefined (newstate.concat state)))))

;; In the same way above we write a pop function that will remove one
;; element from the stack and creates a new stack
;; and returns the popped value and the new stack.

(var pop
  (function ()
    (function (state)
      (var value (get 0 state))
      (var newstate (state.slice 1))
      (array value newstate))))

;; In the case of state monad domonad returns a function which we must call
;; with the initial state. We call the returned function stackOperation.

(var stackOperations 
  (doMonad stateMonad 
    (a (push 5) 
     b (push 10)
     c (push 20)
     d (pop))
    d))

(stackOperations [])

;; ==> [ 20, [ 10, 5 ] ]

Writing your own monad

To write monads use the monad expression. It takes two arguments. The name for the monad, and an object of key value pairs. The keys mResult and mBind are obligatory. The values must be the corresponding monadic result function and the monadic bind function. Optionally you can have the mZero and mPlus keys with their corresponding values. The example below is the definition of the identity monad.

(monad identityMonad
  (object
    "mBind" (function (mv mf) (mf mv))
    "mResult" (function (v) v)))

LispyScript also supports the withMonad expression.

(withMonad name (expression1) (expression2) ... )

Templates

(template name (argument expression) (string expressions) ... )

template takes a name and a list of arguments to be passed to the template as the second argument. The rest of the arguments are string expressions that make up the template. Template returns a compiled template function with the given name which you must call with the arguments to expand the template.

(template-repeat (array expression) (string expressions) ... )

template-repeat takes an array as its first argument and the rest are string expressions that form the template. The template is iterated over for every element of the array and the combined expanded template is returned. Within the template you can access the current element as 'elem' and the current index as 'index'. Note that while 'template' returns a compiled template, 'template-repeat' returns the actual expanded string.

(template-repeat-key (object expression) (string expressions) ... )

template-repeat-key takes an object as its first argument and the rest are string expressions that form the template. The template is iterated over for every key value pair of the object and the combined expanded template is returned. Within the template you can access the current value as 'value' and the current key as 'key'. Note that while 'template' returns a compiled template, 'template-repeat-key' returns the actual expanded string.

HTML5 Templates

Lispysript has a tree structure just like html, we can write html in our templates in a Lispy manner. By including the 'html.ls' module you write HTML Templates like below.


(include "html.ls")

(template page (title links)
  (html {lang:"en"}
    (head
      (title title)
      (script {type:"text/javascript", src:"js/test.js"}))
    (body
      (div {class:"navigation"}
        (ul
          (template-repeat links 
            (li "<a href=\"" elem.href "\">" elem.text "</a>"))))
      (h1 "HTML Templates")
      (p "Welcome to LispyScript HTML templates!"))))

(console.log
  (page
    "My Home Page"
    [{href:"/about", text:"About"},
     {href:"/products", text:"Products"},
     {href:"/contact", text:"Contact"}]))

And below you can see the beautified html output.

<!DOCTYPE html>
<html lang="en">

    <head>
        <title>My Home Page</title>
        <script type="text/javascript" src="js/test.js"></script>
    </head>

    <body>
        <div class="navigation">
            <ul>
                <li>
                    <a href="/about">About</a>
                </li>
                <li>
                    <a href="/products">Products</a>
                </li>
                <li>
                    <a href="/contact">Contact</a>
                </li>
            </ul>
        </div>
        <h1>HTML Templates</h1>
        <p>Welcome to LispyScript HTML templates!</p>
    </body>

</html>

Callback Sequence

LispyScript allows you to avoid writing nested callbacks, by a callback sequence feature. You write your callbacks in a series (sequence). All the functions in the series are exposed to a functions called "next". Calling "next" returns the next callback in the sequence. So just pass the return value of next, when you call a callback. You can see an example with some notes on usage here.

Testing

LispyScript comes with some built in testing features to let you test your code while developing. However if you find that the features offered in the built in tests are not enough, you can use any of the javascript test frameworks available.

(assert (expression) "string message")

The expression must evaluate to "true" and not something truthy, and will return the message with "Passed " or "Failed " added to your message.

(testGroup name (assert1) (assert2) ...)

Create test groups by calling the testGroup expression. It takes a name and one or more asserts. It creates a testGroup which is invoked by the testRunner.

(testRunner groupname "Desription")

Call testRunner to invoke your test group. Pass it the name of the test group and a string description which will be shown at the top of your test results. It returns the test results as a string.

Running in Browser

You can run LispyScript directly in the browser. However this will not be as fast a precompiling LispyScript on the server and loading the compiled JavaScript.

Below is an example on how to use lispyscript in the browser.


// You need to include browser-bundle.js in your html. Go to your js folder and
// type the following command
$ lispy -b
// This will create browser-bundle.js in that folder.
// The example below will alert "4" on the browser. The source of the index.html and
// square.ls is given below. You must open index.html from a server.

index.html
==========
<!DOCTYPE html>
<html lang="en">
<head>
  <title>LispyScript Browser Example</title>

  <script type="text/javascript" src="./js/browser-bundle.js"></script>

  <script type="text/lispyscript" id="square" src="./src/square.ls"></script>

<script type="text/lispyscript" id="main">

(var square (require "square"))
(alert (square 2))

</script>

</head>
<body>
  <div>
      <h1>LispyScript Browser Example</h1>
    </div>
</body>
</html>

square.ls
---------
(set module.exports (function (x)
  (* x x)))

  • Load the "./js/browser-bundle.js" in first.
  • Load your own ".ls" files. Each ".ls" file must have an id.
  • Each LispyScript script tag is treated as a nodejs module. This is unlike JavaScript where every script is loaded in the global scope.
  • The "id" is used to "require" the module in a subsequent module. In the example above we have one ".ls" file loaded ("square.ls"), and the last one is inline. The last module requires the "square" module.
  • All modules are loaded and run on load. So you need to explicitly require a LispyScript module only if it exports something. This is unlike nodejs where you need to explicitly "require" every module.
  • Ideally you should give your modules an "id" which is same as what the require statement on the server side would use. This is important so that the same module can be compiled on the server without any changes if required.

Browser Compatibility

LispyScript 0.3.0 and above only supports IE9 and above. For older browser support use LispyScript 0.2.9.

The compiled Javascript code is compatible with all modern browsers.

However since Lispyscript allows you to call Javascript functions directly from LispyScript, you need to guard against calling non standard native functions. eg. If you call the Javascript "forEach" from LispyScript, this will not work in IE8. So instead use LispyScript "each" instead.

You can also run LispyScript directly in your browser. Thats how the LispyScript tryit page works. However this is not recommended, because compiling your LispyScript and loading the compiled Javascript will always be faster.

Programming Guidelines

  1. You MUST use the "var" keyword when you create a variable. Otherwise this could lead to nasty surprises. This is also true of Javascript.
  2. Variable names starting with three underscores (___varname) are reserverd for LispyScript. You should not start a variable name with three underscores. Except in a special case which we will come to.
  3. Use macros with caution. In most cases a function would serve the purpose. In which case use a function.
  4. Write macros if you want to add new features to LispyScript or create a domain specific Language.
  5. And when you write macros be very careful when you create a variable inside your macro, either using var or as function argument names. Because if the arguments to your macro get dereferenced (eg. ~arg or ~rest...) in the same scope as the variables you created, there is chance for a variable capture situation. This happens when the user of your macro has a variable with the same name and has used it in the argument passed to the macro. You can get around this in the following ways.
    • Obviously if your macro does not create any variables you are fine. There are many macros like that.
    • If your macro is or has an immediately evaluated function, and the macro args get dereferenced as arguments to that function, you are fine again. Because the macro args get dereferenced outside the function. This is also a common situation in LispyScript. As far as possible try to get into this situation.
    • If the variable you create needs to be accessed by the dereferenced code you are fine again
    • If you still need to dereference the macro arg in the same scope as a variable created by you, then makes sure of two things. First that the variable you create is inside a function (usually the case), and second make sure the name starts with three underscores.