Skip to content

Getting Started

Jakub T. Jankiewicz edited this page Aug 12, 2021 · 27 revisions

NOTE: this Guide is for an outdated version that you can access as a stable NPM package. You really should use 1.0.0 Beta version instead. Right now the only documentation for Beta is this wiki page.

Installation

You can use LIPS from unpkg

<script src="https://unpkg.com/@jcubic/lips"></script>

or download from npm:

npm install @jcubic/lips

You can also use webpack to bundle your dependencies together with LIPS and include that one bundle.

Running your first script

After you included the LIPS script into your page. You can put your LIPS code into script tag:

<script type="text/x-lips">
   (load "examples/helpers.lips") ;; helpers file have definition of --> macro
   (--> console (log "hello world!"))
<script>

You can also use src attribute to run your script:

<script type="text/x-lips" src="script.lips"></script>

If you use emacs you can put comment at the beginning

;; -*- scheme -*-

so you will have Scheme syntax highlighting.

Scheme

If you already know scheme here are the differences:

  • no big Standard library,
  • no hygienic macros only pure lisp macros with define-macro syntax and quasi quotation,
  • no tail call eliminations,
  • no call/cc (continuations),
  • symbols in LIPS are always case sensitive,
  • as for now everything is LIPS is created on runtime, including macros and quoted list (it always return new value),
  • macros are first class objects (they can be passed around but they are not functions, so can't be use in map or for-each).

Defining functions

to define a function use define macro:

(define (square x)
  (* x x))

This is equivalent of:

(define square (lambda (x)
                 (* x x)))

and you will get this code if you run:

(macroexpand (define (square x)
               (* x x))) 

Some of the internal macros written in JavaScript are expanding like define.

Macros

To define macros use:

(define-macro (when x . body)
   `(if ,x
        (begin
           ,@body)))

this will create macro when that is like if but you can put more then one expression and it don't have else case.

To learn more about macros I suggest book let over lambda by Doug Hoyte or OnLisp by Paul Graham (both free to read online).

Macros are also first class values like functions and numbers so you can do this:

((let ((def lambda))
    (def (x) (display x))) 10)

JavaScript

You can access JavaScript objects from LIPS, if object is global you can use it like normal variable:

(. document 'getElementById)
(. document "getElementById")

Those two calls will return function getElementById taken from document object. Dot is function that can be used to get the property of an object. You can't use it in every place since it's also special token in LIPS (to mark pair (foo . bar), if you want to use in different place, for instance as variable you can use get function that is alias for dot function).

When you get function property from object (method) the function is bound to that object so you can call it without any issues for example:

(define log (. console 'log))
(log "hello world")

Helper macros

There are also two helper macros that make working with JS easier .. and --> (macros defined in examples/helpers.lips):

(.. document.querySelector)

this is useful for grabbing nested field:

(.. $.terminal.active)

and:

(--> document (querySelector "div"))

you can also use

(--> $.terminal (active))

You can also set Properties on object using set-obj! function:

(set-obj! window "name" "This came from LIPS")

this will set property name on object so you can use:

console.log(window.name);

If you want to generate array from LIPS list you can use function:

((. (list->array '(1 2 3)) 'forEach) (lambda (x) (display x)))

This will convert list (1 2 3) into array get forEach function from that array and call LIPS function as JavaScript in forEach.

(let ((array (list->array '(1 2 3))))
  (--> array (map (lambda (x) (+ 1 x)))))

this will run [].map function, call LIPS code to increase the number and return mapped array.

(for-each (lambda (node)
             (display (--> node.innerHTML (toLowerCase))))
          (array->list ((.. Array.from) (--> document (querySelectorAll "h2")))))

this code will get all h2 tags using Vanila JavaScript convert NodeList to Array and Array to list and print each innerHTML in lower case (they are uppercase on demo page).

new instance of Object

(new Promise (lambda (resolve) (setTimeout (lambda () (resolve 10)) 1000)))

this will create new promise that will resolve after 1 second to number 10.

LNumber

lips.LNumber is special class/constructor that wrap all the numbers in LIPS so if you interact with JavaScript you need to convert those numbers to real numbers, some functions will do this automatically like setTimeout that call valueOf on non numbers. For other cases you need to call this yourself. You can use helper value function from examples/helper.lips file. that will convert numbers to real numbers.

This is done in this way because LNumber wrap multiple behaviors it use bigint native numbers or BN.js library. So you can create really big results. Like factorial of 1000 see test spec file.

Asynchronous code

LIPS will process promises and wait untill they are resolved before it start evaluating rest of the expressions. So your async code is always like using async/await.

Example:

(define-macro (delay time . expr)
  `(new Promise (lambda (resolve)
                  (setTimeout (lambda ()
                                (resolve (begin ,@expr)))
                              ,time))))

(let ((x 10) (y 20))
  (+ 10 (delay 1000 (+ x y))))

Expression will evaluate to 40 after delay. If any of the expression inside is a promise the whole expression will be a promise. You need to know about this and handle properly if you interact with JavaScript.

You can ignore the any promises inside expression by using ignore macro:

(begin (ignore (delay 2000 (display "hello"))) 10)

this will return 10 instantly and then after 2 seconds display "hello". So the value inside ignore should so some side effects because its value will be ignored.

Consider this code:

(--> (fetch "__browserfs__/foo/app.lips") (text))

fetch is function (defined in JavaScript) that return a promise and it can be called with text function (--> helper macro is defined in examples/helpers.lips file).

More complex example, this will return title of the page:

(. (--> (fetch "https://terminal.jcubic.pl") (text) (match /<title>([^>]+)<\/title>/)) 1)

from version 0.20.0 you can simplify this by use this:

(--> (fetch "https://terminal.jcubic.pl") (text) (match /<title>([^>]+)<\/title>/) 1)

jQuery

NOTE: In LIPS demo by default there is loaded jQuery library, you don't need to use it.

In examples/helpers.lips file there is helper macro --> that you can use to interact better with jQuery, you can use chaining:

(--> ($ "body")
     (on "click" (lambda (e) (display "click event")))
     (find "ul")
     (append "<li>Hello</li>")
     (next)
     (css "color" "red"))

It's equivalent of:

$("body")
  .on("click", function(e) { console.log("click event"); })
  .find("ul")
  .append("<li>Hello</li>")
  .next()
  .css("color", "red");

NOTE: you don't need to use chaining you can use --> to call one function.

in helpers.lips file there is also one more macro make-object that allow to create object from key like syntax, so you can use it to create JavaScript objects (e.g. to be used with css() jQuery function or any other JavaScript function):

(let ((css (. ($ ".terminal") 'css)))
  (css (make-object :color "red" :border "1px solid red")))

this function use built in function dot and make-object macro. You can shorten this code using:

(--> ($ ".terminal")
     (css (make-object :color "red" :border "1px solid blue")))

NOTE: to actually change terminal default color you should use :--color since this is css variable used by terminal.

e.g:

(--> ($ ".terminal")
     (css (make-object :--color "black" :--background "white")))

to reverse the colors (css variables, in css method, are supported by jQuery <=3.4.0).

Programming API

You can call LIPS directly inside your JavaScript code (this is how demo was created):

var {exec} = require('@jcubic/lips'); // node
// or
var {exec} = lips; // browser

exec(string).then(function(results) {
     results.forEach(function(result) {
        console.log(result.toString());
     });
});

With exec you can pass 2 more arguments second argument is environment. You can create new Environment using:

var local = lisp.env.inherit('name');
// or 
var local = lisp.env.inherit('name', {
   square: function(x) {
       return x * x;
   }
});
exec('(square 10)', local);

3rd option is optional dynamic scope environment, you can also use true to have dynamic the same as your original environment:

exec(string, local, local); // dynamic scope with local env
exec(string, local, true); // same
exec(string, true); // dynamic scope with LIPS global env

NOTE: exec will always return a promise that resolve to array or results, if single expression is passed as first argument, then it will be array with single element.

Defining LIPS functions

lips.env.set('sum', function(...args) {
   return args.reduce((acc, i) => acc + i);
});

You can use this function in LIPS:

(sum 1 2 3 4 5)
;; 15

Defining LIPS Macros functions:

const {env, Macro, Pair, evaluate, LSymbol } = lips;

// we use _Symbol so we don't overwrite native Symbol (it would break the code).
env.set('delay', new Macro('delay', function(code, {dynamic_scope, error}) {
    var args = {error, env: this};
   
    // this is needed only if you want your function to work with dynamic scope
    args.dynamic_scope = dynamic_scope === true ? this : dynamic_scope;
    return new Promise(function(resolve) {
        setTimeout(function() {
            resolve(new Pair(new LSymbol('begin'), code.cdr));
        }, code.car);
    });
}));

and you can use this code the same as in one of the previous sections:

(let ((x 10) (y 20))
  (+ 10 (delay 1000 (+ x y))))