-
-
Notifications
You must be signed in to change notification settings - Fork 33
v1.0.0
LRational, LComplex, LNumber, LBigInteger, LFloat, LCharacter, LString.
literals #x
(hex) #o
(octal) #b
(binary) #d
(decimal - default) #e
(exact) #i
(inexact)
- Complex -
10+10i
- BigInt -
(** 1000 2000)
- Float -
0.1
or1e-10
- Rational -
1/2
TODO:
The #f is the only fasly value (this is specified by R7RS spec).#f
or false
as the only false value after #87 is done.
to test JavaScript undefined and null you should use null?
predicate. Example:
(let ((x (document.querySelector "missing")))
(if (null? x)
(print "no tag 'missing'")))
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
In LIPS multi-line strings work with indentation. The indent is removed from the output string.
(print "hello
world")
Will print exactly:
hello
world
Lips support all of Unicode as symbols also emoji:
(define 😂 (lambda (x) (string-append x " is laughing")))
(display (😂 "John"))
;; ==> John is laughing
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.
- NodeJS used native file system
- Browser if you include BrowserFS it will fully support all functions
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.
- Lisp macros
(define-macro (do-list name list . body)
`(for-each (lambda (,name)
,@body)
,list))
(do-list item '(1 2 3)
(print item))
- syntax-rules + extension SFRI-46
(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))
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
).
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|)
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.
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.
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
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))
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
- 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
(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#)
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
- 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)))
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!"
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.
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)
- 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?
- 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))
- named lets
- recursive functions
- while macro
- do macro
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"
- repr
- macroexpand
- macroexpand-1
- pprint
- dir
- help
- apropos
__code__
arguments.callee.__code__
__doc__
- you can also use JavaScript:
Object.keys
,Object.values
andObject.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__))))
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.
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.
Copyright (C) 2019-2022 Jakub T. Jankiewicz (Wiki content under Creative Commons (CC BY-SA 4.0) License, but only if you try to publish whole or big part of the content).