Skip to content
Jakub T. Jankiewicz edited this page Jan 6, 2024 · 43 revisions

Data types

LRational, LComplex, LNumber, LBigInteger, LFloat, LCharacter, LString.

Fully supported Scheme Numerical Tower

literals #x (hex) #o (octal) #b (binary) #d (decimal - default) #e (exact) #i (inexact)

  • Complex - 10+10i
  • BigInt - (** 1000 2000)
  • Float - 0.1 or 1e-10
  • Rational - 1/2

False value

TODO: #f or false as the only false value after #87 is done. The #f is the only fasly value (this is specified by R7RS spec).

to test JavaScript undefined and null you should use null? predicate. Example:

(let ((x (document.querySelector "missing")))
  (if (null? x)
      (print "no tag 'missing'")))

Parser constants

This is the list of values that are returned by the parser. They always stay the same even when quoted.

  • true, #true, #t
  • false, #false, #f
  • nil
  • undefined (pending)
  • null
  • NaN, +nan.0, -nan.0

Values in the same row are the same values.

NOTE: those are parser constants, but there are other values that behave the same when quoted:

  • Numbers, Vectors, and Strings

Multiline strings

In LIPS multi-line strings work with indentation. The indent is removed from the output string.

(print "hello
        world")

Will print exactly:

hello
world

Unicode and Emoji

Lips support all of Unicode as symbols also emoji:

(define 😂 (lambda (x) (string-append x " is laughing")))
(display (😂 "John"))
;; ==> John is laughing

Bytevectors

LIPS include macros for those vector literals:

  • #u8
  • #s8
  • #u16
  • #s16
  • #u32
  • #s32
  • #f32
  • #f64

No complex type

And it define functions:

  • make-${type}vector
  • ${type}vector
  • ${type}vector?
  • ${type}vector-length
  • ${type}vector-in-range?
  • vector->${type}vector
  • list->${type}-vector
  • ${type}vector-set!
  • ${type}vector-ref

NOTE: The future support for bytevectors can be added to libraries that needs to be imported.

I/O Ports

  • NodeJS used native file system
  • Browser if you include BrowserFS it will fully support all functions

Lists

lists are created from Pair class instances that have car and cdr properties. They may contain other props like data and ref and cycles that are for internal use. nil object is an instance of Nil class that is always a single object that ends the list.

Macros

  • Lisp macros
(define-macro (do-list name list . body)
   `(for-each (lambda (,name)
                ,@body)
              ,list))

(do-list item '(1 2 3)
  (print item))
(define-syntax for
  (syntax-rules (in as)
    ((for element in list body ...)
     (for-each (lambda (element)
                  body ...)
               list))
    ((for list as element body ...)
     (for element in list body ...))))

(for item in '(1 2 3)
     (print item))

(for '(1 2 3) as item
     (print item))

First-class types

Everything in LIPS is the first-class citizen: Functions, Macros, Environments, Syntax (from syntax-rules), the same as numbers, string, or characters.

(define λ lambda)
(define def define)
(def mul (λ (x) (* x x)))
(mul 2)
;; ==> 4

NOTE: Using macros and Syntax like this may be removed in the future (after adding planned expansion time to fix the issue with syntax-rules).

Objects

To create JavaScript objects literals you can use short syntax:

(define obj &(:foo 10 :bar 20))

in this case, values are always quoted and the object is immutable. to have a mutatable object and have access to variables as values use object macro:

(define obj (let ((x 10) (y 20))
              (object :foo x :bar x)))

This works similar to vector literals and vector function:

You can nest object literals and combine them with vectors:

(define obj &(:name "Jon"
              :last-name "Doe"
              :address &(:street "Long" :number "20/1")
              :hobbies #("sport" "swimming")))

in comparison to JavaScript, you can use special characters in object keys and you can use the same to access the properties:

(print obj.last-name)
;; ==> Doe

(print obj.hobbies)
;; ==> #(sport swimming)

JavaScript bracket notation doesn't work, but if you want to access a property that has spaces you can use quoted symbol syntax (from R7RS):

(define obj (object))
(set! |obj.hello world| "lorem ipsum")
(print |obj.hello world|)
;; ==> lorem ipsum
(dir obj)
;; ==> (|hello world|)

functions

Lambdas (scheme functions) are just JavaScript functions, you can defined them in Scheme and call from JavaScript:

(set-obj! window 'square (lambda (x) (* x x)))
;; or
(set! window.square (lambda (x) (* x x)))
window.square(10);
// ==> LBigInteger {value: 100n, _native: true, type: "bigint"}

as output you will get LIPS data type, you can extract native value using valueOf()

window.square(10).valueOf();

Note: you may loose precision because bigInt will be converted to normal number. You can use this code to prevent that:

function unpack(obj) {
    if (obj instanceof lips.LBigInteger) {
        return obj.__value__;
    }
    return obj.valueOf();
}

You may also want to deal with other number types like LRational and LComplex. You may want to not convert them or maybe convert them to strings.

Functions have length and name properties because they are JavaScript functions created dynamicaly.

(let ((x (lambda (a b)))) x.length)

The name of the function is always "lambda". To get the name of a function defined by:

(define (greet) "Hello World")
greet.__name__
;; ==> "greet"

NOTE: When working with 3rd party libraries or any non Scheme code and you use function as callback the values are automagically unboxed.

function dump_fn(fn, ...args) {
   console.log(fn(...args));
}

if you use this function in LIPS:

(dump_fn (lambda (x) (* x x)) 10)

it will print 100, because of automatic unboxing when using scheme code inside JavaScript code. Because of this behavior, you can use frameworks/libraries like Preact/React and use lambda inside SXML.

define-class

Classes are just syntax sugar over prototype-based code

(define-class Person Object
  (constructor (lambda (self name)
                 (set-obj! self '_name name)))
  (hi (lambda (self)
        (display (string-append self._name " say hi"))
        (newline))))
(define jack (new Person "Jack"))
(jack.hi)

define-class is a macro that creates a prototype-based function object.

Prototypes

You can also use functions to create JavaScript objects yourself (old ES5 class-like system based on prototype inheritance).

(define foo (lambda (x) (set-obj! this "x" x)))
;; same as
(define foo (lambda (x) (set! this.x x)))

(define bar (new foo 10))
(. bar "x")
;; ==> 10
bar.x
;; ==> 10

(set-obj! foo.prototype 'square (lambda (x) (* x x)))
;; same as
(set! foo.prototype.square (lambda (x) (* x x)))

(bar.square 10)
;; ==> 100
(set! foo.prototype.sum (lambda (x) (+ this.x x)))

(bar.sum 5)
;; ==> 15

JavaScript iterators and generators

As version 1.0.0.beta.14 you can't define your own generators in Scheme. Probably you will be able to write a macro in JavaScript that will define lambda* but as of now, you can't define this in LIPS. Because of missing continuations (call/cc) when they will be added you should easily create a macro that implement lambda* like this:

(lambda* (time . args)
   (let loop ((args args))
     (if (not (null? args))
         (begin
            (delay time)
            (yield (car args))
            (loop (cdr args))))))

This is not yet supported. But you can handle generators and iterators that are created in JavaScript.

(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;
                          }
                        })"))

(do-iterator (i (apply gen 100 (range 10))) () (print i))
;; ==> 0
;; ==> 1
;; ==> 2
;; ==> 3
;; ==> 4
;; ==> 5
;; ==> 6
;; ==> 7
;; ==> 8
;; ==> 9

(define (generator->vector generator)
  (let ((result (vector)))
    (do-iterator (i generator)
                  ()
                  (--> result (push i)))
    result))

(print (generator->vector (apply gen 10 (range 10))))

;; ==> #(0 1 2 3 4 5 6 7 8 9)

This is also an example of how to write JavaScript code inside LIPS Scheme.

do-iterator macro have syntax:

(do-iterator (variable iterator)
  (test)
  expression ...)

Example:

(do-iterator (i #(1 2 3 0 1 2 3)) ((zero? i)) (print i))

LIPS environments

All you probably don't need to know about LIPS environment system

They are first-class objects. You can create an instance of it using

(new lips.Environment)

or creating a child environment

(define my-env (lips.env.inherit "tmp"))

If you use exec you can specify your own environment used while evaluating expressions. lips.env is the base user environment, it's a child of the global environment.

(define (display)
   (let ((display (--> lips.env.__parent__ (get "display"))))
     (display "hello")))

(display)
;; ==> hello
(unset! display)
(display "foo")
;; ==> foo

You can overwrite the built-in function but you never actually erase it. You shadow it inside your environment. You can actually change it using this hack:

(let-env lips.env.__parent__
   (unset! display))

this will permanently delete the built-in display function. let-env is a special macro that is very limited it works like let but change the environment that is used inside. One caveat is that you can't use local scope and variables defined outside of the let-env. The only useful function of let-env is that you can load libraries into the main environment to bootstrap the system.

example:

(let-env lisp.env.__parent__
  (load "./lib/std.scm"))

the alternative is (new in beta.9)

(let ((e lisp.env.__parent__))
  (load "./dist/std.scm" e))

This will show the actual environment with your functions and also some default ones added by REPL.

(Object.keys (. (current-environment) '__env__))

let-env is also useful if you want to execute the code in clean environment, example:

(let-env (scheme-report-environment 5)
  (+ 1 2))

TODO:

  • Interpreter object the main API for interacting with the code. if you want to change stdin and stdout you should use lips.Interpreter class. interaction-environment and stdin stdout The interpreter is very simple, what it does, is that it just create new child env from lips.env and store it in a global variable for global env. So you can have only one instance of Interpreter on the page, but you can create your own custom REPL. For any other case in programmatic invocation lips.exec is fine.

This also may be useful if you want to write a library that will use something like BrowserFS to create a file system for input/output ports.

  • Environment has env property that is a plain object with all defined variables.
(let ((x 10))
  (let ((e (current-environment)) (x 20)) ;; e point to outer let
    (+ x e.__env__.x)))
;; => 30
  • get method is a higher-level function that works with nested environments.
(let ((x 10))
  (let ((y 20))
    (let ((e (current-environment)) (x 30))
      (print e.__env__.x)
      (print x)
      (print e.__env__.y)
      (print (--> e (get 'x))))))

;; ==> #<undefined>
;; ==> 30
;; ==> 20
;; ==> 10

Frames are environments created just for functions

  • parent.frame
;; (parent.frame) and (parent.frames)

(define (foo)
  (define x 30)
  (bar))

(define (bar)
  (define x 20)
  (baz))

(define (baz)
   (for-each (lambda (env)
                (let-env env
                  (display x)
                  (newline)))
     (parent.frames)))

(define x 10)
(foo)
;; => 10
;; => 20
;; => 30

Continuations - Not Yet Implemented

TCO - Not Yet Implemented

List Cycles (Circular List)

(define x (let ((x '(1 2 3)))
   (set-cdr! (cddr x) x) x))
(print x)
;; => #0=(1 2 3 . #0#)

from beta.14 you can use R7RS datum labels while defining the lists

(define x '#0=(1 2 . #0#))
(eq? x (cddr x))
#t
(print x)
;; => #0=(1 2 . #0#)

Functional helper functions

Most of them were inspired by RamdaJS library

  • map
  • reduce
  • filter
  • range
  • pluck
  • compose
  • pipe
  • curry
  • take
  • unary
  • binary
  • n-ary
  • every
  • some
  • find
  • flatten
  • complement
  • always
  • once
  • flip
  • unfold

this works:

(map string->number '("10" "20" "30"))

but this don't:

(--> "10:20:30:40" (split ":") (map string->number))

this is because string->number accepts two arguments and JavaScript Array::map passes 3 arguments to its callback function. To fix this you can use unary function, which returns a new function with a limited number of arguments.

(--> "10:20:30:40" (split ":") (map (unary string->number)))

Another example:

(map (n-ary 0 (once random)) (range 10))

(define vector-first (curry (flip vector-ref) 0))
(vector-first #(foo bar baz))
;; ==> foo

(define blank (curry n-ary 0))
(map (blank random) (range 10))
;; ==> list if 10 same random value

(define random-range (pipe range (curry map (blank random))))

(random-range 5)
;; ==> list of 5 random values

JavaScript Promises

  • auto resolving
;; browser
(--> (fetch "https://api.scheme.org") (text) (match #/<h1>([^>]+)<\/h1>/) 1)
;; node.js
(define fs (require "fs"))
(define readFile fs.promises.readFile)

(let ((buff (readFile "README.md")))
  (display (buff.toString)))

Promise quotation

You can quote the promise and handle it like a normal value with '> operator.

(define promise (--> '>(fetch "https://api.scheme.org")
                       (then (lambda (res)
                               (res.text)))
                       (then (lambda (text)
                               (--> text (match #/<h1>([^>]+)<\/h1>/) 1)))))

(print promise)
;; ==> #<js-promise resolved (string)>
(print (await promise))
;; As of 2021-03-19 "Hello!"

Problem with function like Array::forEach

You should never use functions that don't return the values like Array::forEach, because it may create subtle hard-to-find bugs. The problem with Array::forEach is it doesn't return a value. So if inside you write code that evaluates to promise (which can happen with any core functions, you should never assume that some code will not create a promise) it may be evaluated out of sync. See Issue #100 for details.

Creating new syntax that is symmetric with write/read/eval

Default & for an object is created using this syntax see bootstrap.scm for details. Another example is a typed vector.

  • set-special!
  • unset-special!
  • set-repr!
  • unset-repr!

Here is an example of self-evaluating symbols.

(set-special! ":" 'keyword)
(define-macro (keyword n)
   `(string->symbol (string-append ":" (symbol->string ',n))))

And here is an example of a transparent new data type:

(define-class Person Object
   (constructor (lambda (self name) (set-obj! self 'name name))))

(set-special! "P:" 'make-person lips.specials.SPLICE)

(set-repr! Person
  (lambda (x q)
    (string-append "P:(" (repr x.name q) ")")))

(define (make-person name)
   (new Person name))

P:("jon")
;; ==> P:("jon")
(. P:("jon") 'name)
;; ==> "jon"

With this feature you can make any data structure homoiconic (when serializing to string):

(define-class Point Object
   (constructor (lambda (self x y z)
                   (set! self.x x)
                   (set! self.y y)
                   (set! self.z z))))

(set-repr! Point
  (lambda (obj q)
    (string-append "(new Point " (--> (vector obj.x obj.y obj.z) (join " ")) ")")))

(display (new Point 10 20 30))
;; ==> (new Point 10 20 30)

You can serialize any JavaScript class into S-Expression that can restore the object when you evaluate the expression.

This is useful if you want to send data types from server to browser with AJAX, and have Scheme isomorphic code (same code that will run in browser and server - NodeJS).

Example of record type Repr (R7RS record type definition creates a new class object - here <pare>).

(define-record-type <pare>
  (kons x y)
  pare?
  (x kar set-kar!)
  (y kdr set-kdr!))

(define (pare->string pare)
  (string-append "(" (%pare->string pare) ")"))

(define (%pare->string pare)
  (if (pare? pare)
      (let ((rest (kdr pare))
            (first (kar pare)))
        (string-append (if (pare? first) (pare->string first) (repr first))
                       (cond ((pare? rest)
                              (string-append " " (%pare->string rest)))
                           ((eq? nil rest) "")
                           (else
                            (string-append " . " (repr rest))))))
      (repr pare)))

(set-repr! <pare> pare->string)

(define (klist . args)
  (fold-left kons nil args))

(print (klist 1 2 3 4))
;; ==> (1 2 3 4)
(print (pare? (klist 1 2 3 4)))
;; ==> #t
(print (list? (klist 1 2 3 4)))
;; ==> #f
(print (klist (klist 1 2) (klist 3 4)))
;; ==> ((1 2) (3 4))
(print (kons 1 2))
;; ==> (1 . 2)

Exceptions

  • try..catch / try..catch..finally / try..finally
  • throw
(try (throw "hello")
  (catch (e)
    (display (string-append e.message ", world!"))))

WARNING: There is a known bug where try..catch doesn't stop the execution of code after it throws. The issue is tracked in #163.

Regex

  • regex?
  • split
  • join
  • search
  • match
  • replace

Example for JavaScript string methods:

(let ((str "foo"))
   (--> str (match #/^(?:foo|bar)$/)))

Alternative:

(let ((str "foo"))
   (str.match #/^(?:foo|bar)$/))

scheme functions can be used instead:

(let ((str "foo"))
   (match #/^(?:foo|bar)$/ str))

Arguments of those functions are good for function composition:

(define foo-or-bar (curry match #/^(?:foo|bar)$/))
(let ((str "foo"))
   (foo-or-bar str))

Loops

  • named lets
  • recursive functions
  • while macro
  • do macro

Interactive help

In LIPS, each function and macro from the standard library is documented. To find a function or macro you can use:

(apropos "string")
(apropos #/^string/)

The function will return a list of symbols that match the string or regular expression.

You can also use (help ...) macro that will return docstring for a given macro or function.

(help help)
;; (help object)
;;
;; Macro returns documentation for function or macro. You can save the function
;; or macro in variable and use it in context. But help for variable require
;; to pass the symbol itself.

To document your own functions you can use a similar to Python string as the first expression to function (and lambda):

(define (plus x y)
  "(plus x y)

   This function adds two numbers"
  (+ x y))
(help plus)
;; (plus x y)
;; 
;; This function adds two numbers

If the function, have only a string, it will not have a docstring and only return that string.

(define (greet)
   "hello")
(print (greet))
;; ==> "hello"
(help greet)

The same will be with lisp macros (define-macro). You can also document variables:

(define foo 10 "this is foo")
(help foo)
;; ==> this is foo

The same happens for syntax-rules macros, the help needs to be inside define-syntax after the syntax-rules.

(define-syntax foo
  (syntax-rules ()
    ((_) "hello"))
  "(foo)

   This macro expands to text \"hello\"")
(help foo)
;; (foo)
;; 
;; This macro expands to text "hello"

Introspection

  • repr
  • macroexpand
  • macroexpand-1
  • pprint
  • dir
  • help
  • apropos
  • __code__
  • arguments.callee.__code__
  • __doc__
  • you can also use JavaScript: Object.keys, Object.values and Object.entries on any LIPS value.

NOTE: some internals may be hidden from users using JavaScript symbols or nonenumerable properties.

symbols are JavaScript objects with name property

(define x 'foo)
(--> x.__name__ (toUpperCase))
;; ==> "FOO"
'foo.__name__
;; ==> foo.name
(. 'foo '__name__)
;; ==> "foo"

The same as Pairs that make up lists

(let ((l '(1 2 3)))
  (display l.car)
  (newline)
  (display l.cdr)
  (newline)
  (display (cons l.cdr.cdr.car (caddr l))))
;; ==> 1
;; ==> (2 3)
;; ==> (3 . 3)

dir will return list of properties and methods (they are symbols):

(dir '(1 2))
;; ==> (car cdr constructor flatten length find clone last_pair to_array to_object reduce reverse transform map markCycles haveCycles toString set append toDry)

dir will also return methods from lips.Pair.prototype this will return direct properties:

(Object.getOwnPropertyNames (list 1 2))
;; ==> #("car" "cdr" "data")

You can call one of the methods using -->:

(. '(1 2) 'length)
;; ==> #<procedure>
(--> '(1 2) (length))
;; ==> 2

Unfortunately those methods don't have doc strings because they are JS functions. Only standard functions have docs:

(help string-append)

This will display a help message for concat. Some functions are just aliases to core functions written in JavaScript.

(macroexpand (let iter ((i 10)) (if (> i 0) (iter (- i 1)))))
;; ==> (letrec ((iter (lambda (i) (if (> i 0) (iter (- i 1)))))) (iter 10))

named let is macro that can be expanded (let it's written in JavaScript but named let return code like Scheme macros). You can expand any macros, to make code look better you can use pprint:

lips> (pprint (macroexpand (let iter ((i 10)) (if (> i 0) (begin (display i) (iter (- i 1)))))))
(letrec ((iter (lambda (i)
                 (if (> i 0)
                     (begin
                       (display i)
                       (iter (- i 1)))))))
  (iter 10))

Because pprint may be slow on big lists, it's not executed by default.

(define (square x)
  (pprint arguments.callee.__code__)
  (* x x))

(square 10)

(define (baz x) (parent.frames))
(define (bar x) (baz x))
(define (foo x) (bar x))
(define x (car (map (lambda (x) (foo x)) '(1))))

;; here we use enviroment to get arguments property
(for-each (lambda (env)
            (let ((args (--> env (get 'arguments))))
              (pprint args.callee.__code__)))
           (cdr x))




(define (fn x) (+ x x))
;; this expand into (define fn (lambda (x) (+ x x)))
;; you can use macroexpand function

(display (fn 1 2)) ;; print 2
(newline)

(define code fn.__code__)

(set-cdr! (cadr code) (cons 'y nil))
(set-car! (cdaddr code) 'y)

(display (fn 1 2)) ;; prints 3
(newline)


;; Quine in LIPS

((lambda ()
   (pprint (list arguments.callee.__code__))))

SXML and Preact/React

Here is a working example of rendering Preact VDom to a string:

(define preact (require "preact"))
(define h preact.h)
(define jsx->string (require "preact-render-to-string"))

(print (jsx->string (sxml (div (@ (data-foo "hello")
                                  (id "foo"))
                               (span "hello")
                               (span "world")))))

More information about SXML at Wikipedia.

Working Preact example with SXML can be tested on Basic demo and more complex one.

Reading Books about Scheme

LIPS has optional brackets, so you can use it to test code from some Scheme books.

Using parser extensions, you no longer need to change the code when you copy-paste from some books that use different syntax for quotes, an example is the R5RS document

https://www.schemers.org/Documents/Standards/R5RS/r5rs.pdf

or R7RS spec:

http://www.larcenists.org/Documentation/Documentation0.98/r7rs.pdf

that use instead of ' for quotation, all you need to do is:

(set-special! "" 'quote)

and all examples now work without any modifications. You should not use this feature to write quotes using this character but it's nice, that you can, if you're learning or testing code from books.