Core features
Special constants
LIPS define #null
and #void
as Parser constants so they can be used inside quoted expressions:
(let ((lst '(#null #void)))
(write (symbol? (car lst)))
(newline)
(write (symbol? (cadr lst)))
(newline))
;; ==> #f
;; ==> #f
NOTE #null
is the same as JavaScript null
value and it's a false value. Similar to
Kawa Scheme that use #!null
.
#void
constants is the same as result of (value)
or (if #f #f)
. In Scheme it's unspecified value,
but in LIPS it's JavaScript undefined. #void
is not false value.
(eq? (if #f #f) (values))
;; ==> #t
Numerical tower
LIPS support full numerical tower (not yet 100% unit tested):
- integers - using BitInt
- floats - using JavaScript numbers
- rationals
- complex numbers (that can use integers, floats, or rationals)
Print procedure
LIPS define helper print
procedure that display all its arguments with newline after each element.
(print 1 2 3)
;; ==> 1
;; ==> 2
;; ==> 3
Emoji
LIPS fully supports all Unicode characters, including emoji:
(define smiley #\😀)
(define poo #\💩)
(write (string-append (string smiley) " " (string poo)))
;; ==> "😀 💩"
You can also use them as part of symbols (e.g. as variables name):
(define (⏏️)
(print "ejecting"))
(⏏️)
;; ==> ejecting
Macros
LIPS define both Lisp macros and Scheme hygienic macros (syntax-rules
).
It also implements:
- SRFI-46 which allows changing the ellipsis symbol for nested syntax-rules.
- SRFI-139 which allows defining anaphoric syntax-rules macros.
- SRFI-147 which allows defining a new syntax-rules macros to define syntax-rules macros.
Gensyms
With lisp macros you can use gensyms, they are special Scheme symbols that use JavaScript symbols behind the scene, so they are proven to be unique. Additionally you can use named gensym if you pass string as first argument:
(gensym)
;; ==> #:g5
(gensym "sym")
;; ==> #:sym
Single argument eval
Eval in LIPS don't require second argument to eval
. The environment is optional and default
it's a result of calling (interaction-environment)
.
(define x 10)
(eval '(+ x x))
;; ==> 20
(define x 10)
(let ((x 20))
(eval '(- x)))
;; ==> -10
But you can also use the second arguments:
(define x 10)
(let ((x 20))
(eval '(- x) (current-environment)))
;; ==> -20
Read more about LIPS environments.
Procedures
Procedures in LIPS have access additional objects arguments
, but the have nothing to do with JavaScript.
arguments is an array/vector with calling arguments and it have an object callee which points to the same
procedure. So you can create recursive functions with anonymous lambda:
((lambda (n)
(if (<= n 0)
1
(* n (arguments.callee (- n 1))))) 10)
;; ==> 3628800
This is classic factorial function written as lambda without the name.
length property
LIPS functions similarly to JavaScript functions also have length
property that indicate how many
arguments a function accepts. If function get more or less argumenets it's not an error like in Scheme. More arguments are ignored,
and if less arguments are passed they are undefined
(#void
).
(define (sum a b c)
(+ a b c))
(print sum.length)
;; ==> 3
It return number of number arguments the rest (dot notation) arguments are ignored.
(define (sum a b . rest)
(apply + a b rest))
(print sum.length)
;; ==> 3
(sum 1 2 3 4 5 6)
;; ==> 21
Doc strings
Procedures, macros, and variables can have doc strings.
(define (factorial n)
"(factorial n)
Calculate factorial of a given number"
(if (<= n 0)
1
(* n (factorial (- n 1)))))
You can access doc string with help
procedure or with __doc__
property.
(write factorial.__doc__)
"(factorial n)
Calculate factorial of a given number"
If you define variable or hygienic macro with doc string, the string is hidden (you can access it with __doc__
),
so help is the only way to access it:
(define-syntax q
(syntax-rules ()
((_ x) 'x))
"(q expression)
Macro quote the expression")
(write q.__doc__)
;; ==> #void
(help q)
;; ==> (q expression)
;; ==>
;; ==> Macro quote the expression
Typechecking
LIPS do typechecking for all scheme procedures.
(+ "hello" 10)
;; ==> Expecting number got string
You can incorporate typechecking in your own code:
(let ((x 10))
(typecheck "let" x "string" 0))
;; ==> Expecting string got number in expression `let` (argument 0)
(let ((x "string"))
(typecheck "let" x "number"))
;; ==> Expecting number got string in expression `let`
There is also another function to check type of number:
(let ((i 10+10i))
(typecheck-number "let" i "bigint"))
;; ==> Expecting bigint got complex in expression `let`
NOTE: In LIPS all integers are BigInts.
The last typecking function is typecheck-args
that check if all arguments are of same type.
(let ((number '(1 10 1/2 10+10i)))
(typecheck-args "number" "let" number))
;; ==> #void
(let ((number '(1 10 1/2 "string")))
(typecheck-args "number" "let" number))
;; ==> Expecting number got string in expression `let` (argument 4)
Integration with JavaScript
Dot notation
LIPS allow accessing JavaScript objects with dot notation:
document.querySelector
;; ==> #<procedure(native)>
Mutating object properties
You can use dot notation with set!
to change the value:
(set! self.foo 10)
self.foo
top level self
always points to a global object window
in browser or global
in Node.
There is also older API that still work, which is set-obj!
but with dot notation you don't
need it anymore:
(set-obj! self 'foo 10)
(display self.foo)
;; ==> 10
In both platforms you can access global JavaScript objects like normal variables:
(set! self.greet "hello, LIPS")
(write greet)
;; ==> "hello, LIPS"
Date and Time
Since we have full access to JavaScript, we can access the Date
object to manipulate date and time.
(--> (new Date "2024-01-01 12:09:2")
(getFullYear))
;; ==> 2024
(define (format-part number)
"(format-part number)
Convert number to string with leading zero. It should be used
for minutes, hours, and seconds."
(--> number (toString) (padStart 2 "0")))
(let ((date (new Date "2024-01-01 12:09:02")))
(format "~a:~a:~a"
(format-part (date.getHours))
(format-part (date.getMinutes))
(format-part (date.getSeconds))))
;; ==> "12:09:02"
You can also use Date time libraries like date-fns.
Interact with DOM
Here is example how to add button to the page and add onclick handler using browser DOM (Document Object Model) API.
(let ((button (document.createElement "button")))
(set! button.innerHTML "click <strong>me</strong>!")
(set! button.onclick (lambda () (alert "Hello, LIPS Scheme!")))
(let ((style button.style))
(set! style.position "absolute")
(set! style.zIndex 9999)
(set! style.top 0)
(set! style.left 0))
(document.body.appendChild button))
Boxing
LIPS have its own representation for numbers, strings and characters. And when interacting with JavaScript the values may get boxed or unboxed automagically.
You should not confuse boxing with boxes (SRFI-111 and SRFI-195). LIPS boxing are part of implementation of Scheme data types. And SRFI boxes are containers written in Scheme. Name boxing came from JavaScript, when primitive values are wrapped in objects when you try to use them in object context (like accessing a property).
You need to be careful with some of the JavaScript native methods, since they can unbox the value when you don't when them to be unboxed.
Example is Array::push
using with native LIPS types:
(let ((v (vector)))
(v.push 1/2)
(print v))
;; ==> #(0.5)
As you can see the rational number got unboxed and converted into JavaScript float numbers. Unboxing always can make you loose some information because LIPS types needs to be converted into native JavaScript data types. And JavaScript doesn't have a notion of rationals, there are only floating point numbers, and big ints.
Procedures
LIPS Scheme procedures are JavaScript functions, so you can call them from JavaScript.
(set! self.greet (lambda () "hello, LIPS"))
You can call this function from JavaScript
console.log(greet());
// ==> {__string__: 'hello, LIPS'}
Note that the value was not automagically unboxed because we are no longer in LIPS Scheme code and LIPS can't access native
JavaScript. So to get the real a string you need to call valueoOf()
:
console.log(greet().valueOf());
// ==> hello, LIPS
Procedure arity
LIPS don't check the number of argumnents when calling a procedure:
(let ((test (lambda (a b c)
(print a b c))))
(test 10))
;; ==> 10
;; ==> #void
;; ==> #void
The same as with JavaScript if you don't pass an argument it will be undefined. But you still have full compatible with Scheme and use arguments with variable artity:
(let ((test (lambda (first . rest)
(apply print first rest))))
(test 1)
(test 2 3 4))
;; ==> 1
;; ==> 2
;; ==> 3
;; ==> 4
Helper macros and functions
The most useful macro in LIPS (for interacting with JavaScript) is -->
it acts like a chain of
method calls in JavaScript
(--> "this is string" (split " ") (reverse) (join " "))
;; ==> "string is this"
You can chain methods that return arrays or string and call a method of them. The above expression is the same as JavaScript:
"this is string".split(' ').reverse().join(' ');
With --> you can also gab property of a function:
(--> #/x/ (test.call #/foo/ "foo"))
;; ==> #t
(let ((test-bar (--> #/x/ (test.bind #/bar/i))))
(test-bar "BAR"))
;; ==> #t
You can also return a function:
(define test (--> #/x/ test))
(test.call #/foo/ "foo")
;; ==> #t
Read more about function::bind and function::call on MDN.
Legacy macros and functions
There are two legacy macros that are still part of LIPS, but you don't need them most of the time.
The dot operator
.
- dot function was a first way to interact with JavaScript, it allowed to get property from an
object:
(. document 'querySelector)
This returned function querySelector from document object in browser. Note that dot a function can only appear as first element of the list (it's handled in special way by the parser). In any other place dot is a pair separator, see documentation about Pairs in Scheme.
The double dot operator
..
- this is a macro is that simplify usage of .
procedure:
(.. document.querySelector)
Usage of Legacy ..
and .
operators
You still sometimes may want to use .
instead of -->
when you want to get
property from an object returned by expression.
In the old version of LIPS, you have to execute code like this:
((. document 'querySelector) "body")
((.. document.querySelector) "body")
The first expression return a Native JavaScript procedure that is then executed.
This is equivalent of:
(document.querySelector "body")
NOTE the only time when you still need .
function is when you want to get the property of
object returned by expression.
(let ((style (. (document.querySelector "body") 'style)))
(set! style.background "red"))
Here we get a style object from the DOM node without sorting the reference to the DOM node.
NOTE because dot notation in symbols is not special syntax you can use code like this:
(let ((x #(1 2 3)))
(print x.0)
(print x.1)
(print x.2))
;; ==> 1
;; ==> 2
;; ==> 3
Scheme functions
Scheme functions (lambda's) are JavaScript functions, so you can call them from JavaScript.
(set! window.foo (lambda () (alert "hello")))
If you define function like this, in browser REPL, you can call it from JavaScript (e.g. browser developer console).
TODO Screenshot
JavaScript functions
You can call JavaScript functions from Scheme, the same as you call Scheme procedures:
(document.querySelector "body")
;; ==> #<HTMLBodyElement>
In both browser and Node.js you can execute console.log
:
(console.log "hello, LIPS")
;; ==> hello, LIPS
Callbacks
You can use Scheme functions as callbacks to JavaScript functions:
(--> #("1" "2" "3") (map string->number))
;; ==> #(1 +nan.0 +nan.0)
This is classic issue with functions that accept more than one argument. You have samilar issue in JavaScript:
["1", "2", "3"].map(parseInt)
// ==> [1, NaN, NaN]
NOTE: the value are different becaseu in Shceme i
To fix the issue you can define lambda with single argument:
(--> #("1" "2" "3") (map (lambda (str) (string->number str))))
;; ==> #(1 2 3)
You can also use one of functional helpers insprired by Ramda:
(--> #("1" "2" "3") (map (unary string->number)))
;; ==> #(1 2 3)
The unary
higher-order procedure accept a single
procedure and return new procedure that accept only one argument.
To read more check Functional helpers.
WARNING be careful when using scheme callback functions inside JavaScript.
Since some code may be async
and your code may break.
Example of procedures that are not wise to use are:
Array::forEach
- this function accepts a callaback but because it doesn't return anything, LIPS can't automatically await the response, and your code may execute out of order.String::replace
- this function can accept optional callback and iflambda
is async you will end up with[object Promise]
in output string. Any macro or function can return a promise in LIPS, and if any of the expression inside a function return a Promise, the whole function return a Promise and become async. Here is example code that demonstrate the problem:
(--> "foo bar" (replace "foo" (lambda () (Promise.resolve "xxx"))))
"[object Promise] bar"
Instead of Array::replace
you should use LIPS Scheme replace
procedure that works with async lambda
:
(replace #/[a-z]+/g (lambda ()
(Promise.resolve "lips"))
"foo bar")
;; ==> "lips lips"
Regular Expressions
LIPS define regular expressions it uses native JavaScript regular expressions.
At first, the syntax looked like in JavaScript. It was problematic for the parser
so you were not able to put space after /
to distinguish from divide procedure.
Later, the syntax was renamed into form that start with hash #/[0-9]/
. The same
syntax is used by Gauche implementation. But LIPS supports more flags (same as JavaScript).
Vectors
In LIPS Scheme vectors are JavaScript arrays. So you can execute methods on them with -->
macro:
(--> #("one" "two" "three") (join ":"))
;; ==> "one:two:three"
Object literals
In LIPS you can define object literals with &
syntax extension:
(define obj &(:name "Jack" :age 22))
(write obj)
;; ==> &(:name "Jack" :age 22)
(console.log obj)
;; ==> { name: 'Jack', age: 22 }
You can nest object literals and mix them with different object:
(define obj &(:name "Jack" :hobbies #("swimming" "programming")))
(write obj.hobbies)
;; ==> #("swimming" "programming")
(console.log obj)
;; ==> { name: 'Jack', hobbies: [ 'swiming', 'programming' ] }
Object similar to Scheme vectors, are immutable, and everything inside is quoted automatically:
(define obj &(:name Jack))
(write obj)
;; ==> &(:name "Jack")
But to make it possible to share objects with JavaScript, native LIPS values are automatically unboxed. So instead of symbol representation you get a JavaScript string.
You can also use quasiquote with object literals:
(define jack (let ((name "Jack")
(age 22))
`&(:name ,name :age ,age)))
(write jack)
;; ==> &(:name "Jack" :age 22)
NOTE: because of the construction of syntax extensions and quasiquote, you can't splice a list inside object literals:
(let ((args (list ':foo "lorem" ':bar "ipsum")))
`&(,@args))
;; ==> pair (unquote-splicing args) is not a symbol!
The reason why this work like this is because, syntax extensions (&
) runs at parse time and LIPS macros are runtime.
This may change in the future when expansion time will be implemented.
Objects also have longhand form with object
macro:
(let ((obj (object :name 'Jack)))
(write obj))
;; ==> &(:name "Jack")
But note that object macro is async (return a Promise) so it may be problematic when used it with native JavaScript code.
Using long form (object)
syntax you can use splicing with help of eval
:
(let ((args '(:foo "lorem" :bar "ipsum")))
(eval `(object ,@args)))
;; ==> &(:foo "lorem" :bar "ipsum")
The same you can use macros that will return LIPS Scheme code:
(define-macro (create-object . args)
`(object ,@args))
(create-object :foo "lorem" :bar "ipsum")
;; ==> &(:foo "lorem" :bar "ipsum")
NOTE: this example macro works the same object
is it's not that useful, but you can create
more complex code where you will be able to generate object literals with splicing.
Object literal also have shorthad notation:
(let ((obj &(:x :y)))
(write obj))
;; ==> &(:x #void :y #void)
It creates two writtable slots, the rest of the props are read only:
(let ((obj &(:x :y)))
(set! obj.x 10)
(set! obj.y 20)
(write obj))
;; ==> &(:x 10 :y 20)
(let ((obj &(:x :y)))
(set! obj.z 20)
(write obj))
;; ==> Cannot add property z, object is not extensible
(let ((obj &(:x :y :z 10)))
(set! obj.z 20)
(write obj))
;; ==> Cannot assign to read only property 'z' of object '#<Object>'
Automagic async/await
LIPS do automatic async/await so it waits for any promise before evaluating next expression.
(Promise.resolve "xxx")
;; ==> "xxx"
This simplifies code when using promises, for instance using fetch API (AJAX).
(--> (fetch "https://scheme.org.pl/test/") (text) (match #/<h1>([^>]+)<\/h1>/) 1)
;; ==> "Scheme is Super Fun"
This is equivalent of JavaScript using async/await:
cons res = await fetch("https://scheme.org.pl/test/");
const text = await res.text();
text.match(/<h1>([^>]+)<\/h1>/)[1];
Promise quotation
Sometimes you need to process promises as values, for this LIPS support quotation
of promises. You escape automagic async/await realm and get access to promise as value:
to quote a promise you use '>
syntax extension. To again get into
automatic async/await you can use (await)
procedure
(let ((promise (--> '>(fetch "https://scheme.org.pl/test/")
(then (lambda (res)
(res.text)))
(then (lambda (text)
(. (text.match #/<h1>([^>]+)<\/h1>/) 1))))))
(print (await promise)))
;; ==> Scheme is Super Fun
NOTE Inside then
lambda promises are still automagically resolved.
(--> '>(Promise.resolve "hello")
(then (lambda (value)
(print (string-append value " " (Promise.resolve "LIPS"))))))
;; ==> hello LIPS
Promises vs delay expression
Don't confuse JavaScript promises with delay
expressions. Their representation looks similar:
(delay 10)
;; ==> #<promise - not forced>
'>(Promise.resolve 10)
;; ==> #<js-promise resolved (number)>
You can check if a value is a promise by quoting the expression and using promise?
predicate:
(let ((a '>10)
(b '>(Promise.resolve 10)))
(print (promise? a))
(print (promise? b)))
;; ==> #f
;; ==> #t
Exceptions
LIPS Scheme use javascript exception system. To throw an exception you use:
(throw "This is error")
;; ==> Error: This is error
or
(raise (new Error "error"))
The raise
procedure throw any object and throw
wraps the argument in new Error
.
You can catch exceptions with LIPS specific try..catch..finally:
(try
(throw "nasty")
(catch (e)
(print (string-append "error " e.message " was caught"))))
;; ==> error nasty was caught
You can also have finally expression:
(try
(throw "nasty")
(catch (e)
(print (string-append "error " e.message " was caught")))
(finally
(print "nothing happened")))
;; ==> error nasty was caught
;; ==> nothing happened
You can also define finally
without catch
:
(try
(throw "nasty")
(finally
(print "after error")))
;; ==> after error
;; ==> nasty
NOTE the order of execution is not expected, but it may change in the future.
LIPS also define R7RS guard procedure
that is just a macro that use try..catch behind the scene:
(guard (e ((list? e) (print (string-append "Error: " (car e)))))
(raise '("error")))
;; ==> Error: error
JavaScript Generars and iterators
Right now there is no way to define JavaScript generators inside LIPS. You can create iterator using iteration prorocol, But to have yield keyword you need continuations, they are part of the LIPS Roadmap.
Here is example of creating iterator in LIPS:
(let ((obj (object))
(max 5))
(set-obj! obj Symbol.iterator
(lambda ()
(let ((i 0))
`&(:next ,(lambda ()
(set! i (+ i 1))
(if (> i max)
`&(:done #t)
`&(:done #f :value ,(/ 1 i))))))))
(print (iterator->array obj))
(print (Array.from obj)))
;; ==> #(1 1/2 1/3 1/4 1/5)
;; ==> #(1 1/2 1/3 1/4 1/5)
Array.from
can't be used for every possible case because it will unbox the values (and convert
rational to float), here it doesn't happen because LIPS don't treat JavaScript iterators in any
special way (it may change in the future). But Array.from
will convert the array of rationals to
float if used on normal vector:
(Array.from #(1/2 1/3 1/4 1/5))
;; ==> #(0.5 0.3333333333333333 0.25 0.2)
NOTE: be careful when using iterator protocol because any function side Scheme can return a promise. If you would change
quoted object literal `&()
with longhand object
you will get an error because object
is async.
You can abstract the use of iteration protocol with a macro, but to have real yield
keyword like
syntax you need call/cc
.
You can also define generators inside JavaScript using self.eval
(JavaScript global eval
):
(define gen (self.eval "(async function* gen(time, ...args) {
function delay(time) {
return new Promise((resolve) => {
setTimeout(resolve, time);
});
}
for (let x of args) {
await delay(time);
yield x;
}
})"))
(iterator->array (gen 100 1 2 3 4 5))
;; ==> #(1 2 3 4 5)
Here is example of async generator written in JavaScript.
Classes
In LIPS, you can define JavaScript classes with define-class
macro:
(define-class Person Object
(constructor (lambda (self name)
(set! self.name name)))
(greet (lambda (self)
(string-append "hello, " self.name))))
(define jack (new Person "Jack"))
(write jack)
;; ==> #<Person>
(jack.greet)
;; ==> "hello, Jack"
define-class
is macro written in Scheme that uses
JavaScript prototypes behind the scene.
The class always need to have a base class (parent) or you need to use null
. Classes have explicit
self
as first argument (similar to Python) but this
also works inside functions:
(set! jack.run (lambda () (string-append "run, " this.name)))
(jack.run)
;; ==> "run, Jack"
To create the new instance of a Class, you can use new
procedure.
You can also manipulate JavaScript prototypes directly:
(write Person.prototype)
;; ==> #<prototype>
(set! Person.prototype.toString (lambda () (string-append "#<Person (" this.name ")>")))
(display (jack.toString))
;; ==> #<Person (Jack)>
By default toString is not used for representation of objects, but you add representation if you want. See Homoiconic data types.
Node.js
In Node.js, you can load JavaScript modules with require
:
(define fs (require "fs/promises"))
(let ((fname "tmp.txt"))
(fs.writeFile fname "hello LIPS")
(write (fs.readFile fname "utf-8")))
;; ==> "hello LIPS"
In above code, you can see example of automagic async/await.
If you have to use callback based API in Node, use promisify function from Module util.
You can also use the Promise
constructor yourself:
(define fs (require "fs"))
(define-macro (async expr)
(let ((resolve (gensym "resolve"))
(reject (gensym "reject")))
`(new Promise (lambda (,resolve ,reject)
,(append expr (list `(lambda (err data)
;; Node.js error is null when no error
(if err
(,reject err)
(,resolve data)))))))))
(let ((fname "tmp.txt"))
(async (fs.writeFile fname "Hello, LIPS!"))
(write (async (fs.readFile fname "utf-8"))))
;; ==> "Hello, LIPS!"
In the above example, we import a regular callback based fs module and use the Promise
constructor
abstracted away with a lisp macro.
ES Modules
LIPS Scheme runs in ES Module, but import
is reserved for experimental R7RS modules. If you want
to import module that is ESM only. You need to access JavaScript dynamic import. But global.import
is not defined. If you want to define JavaScript dynamic import, you can use code:
(define $:import (global.eval "(x) => import(x)"))
You can use this function like this:
(--> ($:import "fs/promises") (readFile "README.md" "utf8"))
But it will also work with ESM only module that can't be imported with require
.
Finding LIPS Scheme directory
With help from (require.resolve)
you can get the path of the root directory of LIPS Scheme:
(--> (require.resolve "@jcubic/lips") (replace #/dist\/[^\/]+$/ ""))
Node.js REPL load lips from Common.jS file and require.resolve
returns path to file
dist/lips.cjs
, by removing with with String::replace and regular expression you can the real path
to the root of the LIPS Scheme.
Binary compiler
LIPS Scheme have dumb binary compiler. The compiler is a way to compress the LIPS Scheme code and create binary file that is faster to load. Compiler is use to make bootstrapping faster. The binary file use CBOR serialization format that is then compressed with LZJB algorithm that is pretty fast. And it can still be compress further with gzip by the HTTP server.
To compile/compress a file you can use -c
flag when executing lips
executable.
$ lips -c file.scm
You can then execute the code with:
$ lips -c file.xcb
Will create file.xcb
in same directory. For smaller files it make not have a difference when
loading .xcb
or .scm
files.
NOTE: directives #!fold-case
and #!no-fold-case
work only inside the parser and they are
treated as comments, so you can't compile the code that have those directives.
loading SRFI
In LIPS, you can use (load)
with path absolute or relative to the directory of the executed LIPS
file. But you can use special syntax that indicate the root directory of the LIPS Scheme.
(load "@lips/lib/srfi/1.scm")
This will load the code from SRFI-1 List Library. You can use this syntax in Node based REPL (NPM executable). The same syntax should work with the web. But note that the root directory reply on the path of the LIPS Scheme script file. So you if you bundle the code with Webpack or Rollup, LIPS may not find the root URL and may not be able to load the proper file.
Limitations
LISP Scheme currently don't support continuations and Tail Call Optimization. But they are part of the roadmap for version 1.0.