Implementation in JavaScript that compiles to JavaScript and then evals the resulting code. Goal is maximum interoperability with JavaScript. Large parts are lifted from Scheme and Clojure.
(and "" 0) ; ""
(and 0 "") ; 0
(and "a" 1) ; 1
(and 1 "a") ; "a"
(define nums [1 2 3 4])
(apply + nums) ; 10
Arrays are supported as literals.
(define primes [2 3 5 7 11])
(define key "foo")
(define value 42)
(define tuple [key value]) ; ["foo" 42]
true
and false
map directly to true
and false
in JavaScript.
;; Calling a procedure compiles directly to a JavaScript function call
(f x y) ; f(x, y)
;; Applying a sequence as (the rest of the) arguments is possible
(f x y . z) ; f(x, y, ...z)
(f x y . (list 2 3)) ; doesn't work, results in f(x, y, list, 2, 3)
;; Defining a procedure compiles directly to a JavaScript function
(lambda (x y . z) z) ; (x, y, ...z) => z
Conact two sequences.
(concat [1 2] `(3 4)) ; (1 2 3 4)
(cond
(= x 10) "x is 10"
(> x 10) "x greater than 10"
true "x less than 10")
Use define
to create a reference to a value at the top level of a module.
(define SIZE 64)
Note that define
can be reused to fix code when using the repl.
;; somewhere in a session
(define square (lambda (x) (+ x x)))
;; later in that session
(define square (lambda (x) (* x x)))
Shorthand for defining a macro.
(defmacro infix (a op b) (list op a b))
Shorthand for defining a function.
(defn square (x) (* x x))
(if flag
(do
(log "something")
(log "something else")))
(doto js/console (.log "Hello") (.log "World"))
Create a function, with destructuring.
((fn ((x y) (u v)) [(+ x u) (+ y v)]) [1 2] [3 4]) ; [4 6]
Canonical way to make a choice.
(if (= x 10) "decimal" "other")
The predicate follows the truthy/falsy conventions of JavaScript, as the result from the compiler shows clearly.
(if x y z) ; (x ? y : z)
Used to import another module.
(import mandelbrot ../fractals/mandelbrot.clj)
(mandelbrot/render -2 -2 4)
The module can by any (commonjs) module.
(import fs fs)
(fs/readFileSync "hello.txt")
To enhance the interop with JavaScript, sequences implement the iterable interface (i.e. Symbol.iterator
). So a lisp function returning a sequence can be used directly in JavaScript.
;; example.clj
(define getSeq (lambda () (seq 2 (seq 3 (seq 5 ())))))
const example = require("./example.clj")
console.log([...example.getSeq()]) // [2, 3, 5]
The core of any lisp is definitely lambda calculus.
((lambda (x y z) y) 1 2 3) ; 2
((lambda (x y . z) z) 1 2 3 4) ; [3 4]
;; the lambda body can be multiple expressions
(lambda add (a b)
(log "adding...")
(+ a b))
(let [x (+ 1 2) y (+ 3 4)]
(* x y)) ; 21
List implements the traditional singly linked list in a lisp using cons
cells.
(define lst (cons 2 (cons 3 ()))) ; (2 3)
(define lst2 (list 1 2 3 4)) ; (1 2 3 4)
Note that the cdr
of a cons
cell doesn't have to hold another cons
, but the result won't be a sequence.
(define pair (cons 2 3))
Log JavaScript value directly to the console.
(log {1 2 3 4}) ; {"1": 2, "3": 4}
JavaScript is not so good with proper tail calls, so to achieve proper iterations loop/recur should be used.
(loop (x 6 s 0)
(if (> x 0)
(recur (- x 1) (+ s x))
s)) ; 21
A macro is a function that takes a data structure, presumable representing code, and return transformed code. Macros are invoked before compilation to JavaScript in a phase called macro expansion.
(define infix (macro (x f y)
(list f x y)))
(infix 3 + 4) ; 7
A syntax quote and unquote are provided to make reading and writing macros simpler.
(define x 10)
`(= ~x y) ; (= 10 y)
(map (fn (x) (+ x 1)) [1 2 3]) ; (2 3 4)
Just like in JavaScript, number
is the only numeric type known.
(+ 24 18) ; 42
Note there is no numeric tower.
null
maps directly to null
in JavaScript. Note that nil
does not exist.
Objects are supported as literals.
(define age 6)
(define cat { "name" "snuggles" "age" age })
Note that keys will always end up as strings.
{ 42 42 } ; { "42" 42 }
(or 0 "") ; ""
(or "" 0) ; 0
(or 1 "a") ; 1
(or 0 "a") ; a
Write a value to the console using the lisp printer.
(println {1 2 3 4}) ; {"1" 2 "3" 4}
The ISeq
interface comprises of three functions: empty?
, first
and rest
. Data structures implementing are arrays, objects, lists and of course sequences.
seq
can be used to build lazy sequences.
(define ones (seq 1 ones))
(first ones) ; 1
(first (rest ones)) ; 1
Note that an empty sequence is given by ()
.
(seq 3 ()) ; (3)
String such as "hello world"
map directly to string
in JavaScript.
Lisp symbols are implemented using JavaScript Symbol
. Should you need interop with lisp using symbols (unlikely) here is an example.
const equations = require("./equations.clj")
// solve x * x = 4
console.log(
equations.solve([
Symbol.for("="),
4,
[Symbol.for("*"), Symbol.for("x"), Symbol.for("x")]
])
)
(take 5 [1 2 3 4 5 6 7 8 9 10]) ; (1 2 3 4 5)
undefined
maps directly to undefined
in JavaScript.
The usual whitespace
- spaces
- newlines
- carriage returns
- tabs
But also
- commas!
So both are valid.
(define x [1 2 3])
(define y [1, 2, 3])
(zip + [1 2 4] [8 16]) ; (9 18)
- proper source maps for debugging
- consistent namespacing: / for namespace (files really and static fields)
- js/Math/PI
- (. js/console log) or (. log js/console)?
- avoid primitives being in env for every module
- make loop/recur (loop [x 1 y 2]) using []?
- Throw error on unknown symbol
- make env richer?
- env("list")?
- macroexpand - (quote (let [x 10] x))
- arity?
- destructure {}
- is ("foo" {}) a good idea?
- should ("foo" null) return undefined?
- define seq as macro and using deftype implementing ISeq protocol?
- compile.js: clash between / (division) and namespaces (. js/Math PI)
- implement syntax quote as macro expansion