-
Notifications
You must be signed in to change notification settings - Fork 0
02 Language
This overview mainly outlines the macros that come from pure Sibilisp, therefor it is more of a reference sheet. However, it should be suitable to get you up and running in Sibilisp.
For more information about the language Sibilisp is based upon, don't forget to consult the Sibilant docs. Sibilisp builds upon what Sibilant provides and takes special care that pure Sibilant files can be compiled as well. Therefor, all macros from Sibilant work under Sibilisp.
Sibilisp is based on JavaScript and as such, there are certain language-level features that nothing based on JS can provide - for example, a full-blown implementation of pattern matching in the sense of Haskell for example is impossible to implement. For the same reasons, the following sentence is to be considered:
Parts of JavaScript rely on statements rather than expressions (which is completely fine for JavaScript but not for Sibilisp), therefor some special forms of certain macros have to be used in such places. The following table shows the available macros and their counterparts which are postfixed with a *
:
Normal macro | Statement |
---|---|
(if ... ) |
(if* ... ) |
(while ... ) |
(while* ... ) |
(for ... ) |
(for* ... ) |
Below you'll find a table of all types that Sibilisp supports.
Type | JavaScript | Sibilisp |
---|---|---|
Undefined | undefined |
(void) |
Null | null |
(nil) |
String |
"abc" 'abc'
|
"abc" |
Boolean |
true false
|
true false
|
Int (Number) | 1000000 |
1000000 / 1,000,000
|
Float (Number) | 1.5 |
1.5 |
RegExp | /\w+/g |
(regex "\\w+" 'g) |
Array | [1, 2, 3] |
(list 1 2 3) |
Object | {a: 1, b: 2} |
(hash a 1 b 2) |
Function | function fn() { .. } |
(defun fn () ..) |
Lambda | (a) => a |
(lambda (a) a) / (#(a) a)
|
Date | new Date(2020, 0, 1) |
(date 2020 0 1) |
Promise | new Promise( .. ) |
(future ..) |
Generator | function* fn() { .. } |
(generator fn () ..) |
Map | new Map([['a', 1]]) |
(dict 'a 1) |
Set | new Set([1, 2, 3]) |
(mset 1 2 3) |
Error | new Error("message") |
(error "message") |
All listed types have built-in type checking macros that end with a question mark. All of them accept multiple values.
JavaScript | Sibilisp |
---|---|
a === null |
(nil? a) |
a === undefined |
(void? a) |
Number.isNaN(a) |
(nan? a) |
typeof a === 'string' |
(string? a) |
typeof a === 'number' && !Number.isNaN(a) |
(number? a) |
`typeof a === 'bigint' | (bigint? a) |
typeof a === 'function' |
(function? a) |
Array.isArray(a) |
(list? a) |
a != null && a.constructor === RegExp |
(regex? a) |
a != null && a.constructor === Object |
(hash? a) |
a != null && a.constructor === Map |
(dict? a) |
a != null && a.constructor === Set |
(mset? a) |
a != null && a.constructor === Date |
(date? a) |
a != null && a.constructor === Promise |
(future? a) |
a != null && a.constructor === GeneratorFunction |
(generator? a) |
a == null |
(nothing? a) |
a != null |
(exists? a) |
typeof a !== 'undefined' |
(defined? a) |
a instanceof Error |
(error? a) |
Sibilisp provides several type coercion macros:
JavaScript | Sibilisp |
---|---|
String(a) |
(as-string a) |
Number(a) |
(as-number a) |
BigInt(a) |
(as-bigint a) |
Boolean(a) |
(as-boolean a) |
Array.from(a) |
(as-list a) |
The (error)
macro has a special variant, that allows to throw errors and interrupt the program, called (error!)
.
(error "Message") ; new Error("Message")
(error! "Message") ; throw new Error("Message")
Because the target language is JavaScript, Sibilisp supports the same numeric data types Number
and BigInt
.
Several comparison operators are available for Sibilisp:
JavaScript | Sibilisp |
---|---|
a == b |
(eq? a b) |
a === b |
(eql? a b) |
a < b |
(< a b) |
a <= b |
(<= a b) |
a > b |
(> a b) |
a >= b |
(>= a b) |
Sibilisp supports the usual arithmetics.
(+ 4 2 1) ; 4 + 2 + 1
(- 4 2 1) ; 4 - 2 - 1
(* 4 2 1) ; => 4 * 2 * 1
(/ 4 2 1) ; => 4 / 2 / 1
(** 4 2 1) ; => 4 ** 2 ** 1
(mod 4 2 1) ; => 4 % 2 % 1
Sibilisp also provides macros for all static methods of the Math
object and, of course, all common math related operators.
JavaScript | Sibilisp | Note |
---|---|---|
Math.random() |
(random) |
Allows passing in a seed value |
Math.max(a, b) |
(max a b) |
|
Math.min(a, b) |
(min a b) |
|
Math.floor(a) |
(floor a) |
|
Math.ceil(a) |
(ceil a) |
|
Math.round(a) |
(round a) |
|
Math.fround(a) |
(float32-round a) |
|
Math.sin(a) |
(sine a) |
|
Math.cos(a) |
(cosine a) |
|
Math.asin(a) |
(asine a) |
|
Math.asinh(a) |
(asin-h a) |
|
Math.acos(a) |
(acos a) |
|
Math.tan(a) |
(tan a) |
|
Math.atan(a) |
(atan a) |
|
Math.atan2(a, b) |
(atan-2 a b) |
|
Math.atanh(a) |
(atan-h a) |
|
Math.sqrt(a) |
(square-root a) |
|
Math.cbrt(a) |
(cube-root a) |
|
Math.exp(a) |
(exp a) |
|
Math.expm1(a) |
(exp-m1 a) |
|
Math.pow(a, b) |
(power a b) |
|
Math.hypot(a, b) |
(hypot a b) |
|
Math.log(a) |
(loga a) |
|
Math.log10(a) |
(loga-10 a) |
|
Math.log1p(a) |
(loga-1p a) |
|
Math.log2(a) |
(loga-2 a) |
|
Math.abs(a) |
(abs a) |
|
Math.trunc(a) |
(trunc a) |
|
Math.sign(a) |
(sign a) |
Functions are one of the main building blocks of every Sibilisp program. Named functions can be defined using the (defun .. )
macro:
(defun add-five (n)
(+ n 5))
Sibilisp's functions do support positional and rest parameters, the same way JavaScript does.
(defun div-zero-safe (a b ...rest)
(let ((ns (.filter rest (lambda (n) (not (= 0 n))))))
(/ (ternary (eql? a (void)) 1 a)
(ternary (eql? b (void)) 1 b)
...ns)))
For positional arguments, before Sibilisp reached version 0.8.0, a default value would be set through the (default .. )
macro. Since version 0.8.0, Sibilisp also supports default arguments without using (default .. )
, and instead wrapping the argument and the default value in additional parantheses. The aforementioned (div-zero-safe .. )
function can then be rewritten like this:
;; Default arguments before 0.8.0
(defun div-zero-safe (a b ...rest)
(default a 1 b 1)
(let ((ns (.filter rest (lambda (n) (not (= 0 n))))))
(/ a b ...ns)))
;; Default arguments since 0.8.0
(defun div-zero-safe ((a 1) (b 1) ...rest)
(let ((ns (.filter rest (lambda (n) (not (= 0 n))))))
(/ a b ...ns)))
Argument destructuring is supported, too. For example, to destructure a (hash .. )
object, write curly parans around the property names you want to use. However, it isn't possible to use argument destructuring and a default value together.
Examples of (hash)
and (list)
destructuring are shown below:
(defun sum-a-b ({ a b })
(+ a b))
(sum-a-b (hash :a 5 :b 10 :c 20)) ; => 15
(defun sum-first-two ([ a b ])
(+ a b))
(sum-first-two (list 5 10 20)) ; => 15
Lambdas are anonymous functions that you can create in two ways:
Using the (lambda ..)
macro or it's alias (# ..)
. It's recommended to use #
.
(defun pad-all (items (s "it-") (n 2))
(.map items (#-> (as-string)
(.pad-start n s))))
INFO
Lambdas have the same capabilities that functions defined with(defun .. )
have in regards to arguments and destructuring , but you need to define default argument values with the(default .. )
macro.
TIP
Make sure to read about lambdas in the Sibilant docs
To create dates, Sibilisp provides a (date)
, and some related macros.
(date 2020 0 1 18 5 57) ; new Date(2020 ... )
(defvar now-utc (date-now-utc))
(defvar now (date-now))
(defvar jan-1st-2020 (date 2020 0 1))
(date-clone jan-1st-2020)
(defvar jan-1st-2020 (date-parse '2020-01-01'))
Note:
(date-parse)
transpiles into JavaScript'sDate.parse
and therefor underlies some restrictions in what ISO strings can be passed in. An explanation can be found here: MDN
You can use Sibilant's (list)
macro as well as all other list-related macros. Sibilisp adds the list-clone
macro for symmetry reasons with the other types.
(list "a" "b" "c") ; ["a", "b", "c"]
For list access, Sibilisp also ships specialized macros that complement those coming with Sibilant.
(defvar nums (list 1 2 3 4))
(first nums) ; => 1
(second nums) ; => 2
(third nums) ; => 3
(last nums) ; => 4
(butlast nums) ; => (list 1 2 3)
(rest nums) ; => (list 2 3 4)
(defvar nums (list 1 2 3 4))
(list-clone nums) ; => (list 1 2 3 4)
Note:
Sibilant allows to write[]
for list creation and you can use this style if you like, however it is discouraged because it makes it less clear when creation or destructuring takes place. Another argument is that by using square brackets, list creation uses completely different syntax than - for example -(dict)
creation uses.
A (hash)
in Sibilisp corresponds to a plain JavaScript Object
. You can create and destructure hash values
into variable values. Hash values can also be created as JSON-compatible structures, if you follow the rules:
(defvar jdoe (hash 'name (list "John" "Doe")
'user-id "ujd2...t"))
Destructuring still works the same:
(defvar { name user-id } jdoe)
This will (roughly) compile into:
let jdoe = { "name": ["John", "Doe"], "userId": "ujd2...t" };
let name = jdoe.name,
userId = jdoe.userId;
(defconstant *a* (hash :a 0)
*b* (hash :b 1))
;; Performs a pure/non-destructive merge, returns a
;; new hash with *a* and *b* merged into
(hash-merge *a* *b*)
;; Performs an unpure/destructive merge, alters
;; the *a* constant
(hash-merge! *a* *b*)
(defconstant *a* (hash :a 0))
(hash-pairs *a*) ; => (list (list a 0))
(defconstant *a* (hash :a 0))
(hash-keys *a*) ; => (list "a")
(defconstant *a* (hash :a 0))
(hash-values *a*) ; => (list 0)
(hash-create (getf instance 'prototype)) ; Object.create(instance.prototype)
(hash-create) ; Object.create(null)
(hash-clone a)
Note:
Sibilant allows to write{}
for hash creation and you can use this style if you like, however it is discouraged because it makes it less clear when creation or destructuring takes place. Another argument is that by using curly braces, hash creation uses completely different syntax than - for example -(dict)
creation uses.
The (dict .. )
structure corresponds to the JavaScript Map
structure.
(dict 'a 1 2 (gensym "b"))
(defconstant *a* (dict 'a 0)
*b* (dict 'b 1))
;; Performs a pure/non-destructive merge, returns a
;; new dict with *a* and *b* merged into
(dict-merge *a* *b*)
;; Performs an unpure/destructive merge, alters
;; the *a* constant
(dict-merge! *a* *b*)
(defconstant *a* (dict 'a 0))
(dict-pairs *a*) ; => (list (list 'a 0))
(defconstant *a* (dict 'a 0))
(dict-keys *a*) ; => (list "a")
(defconstant *a* (dict 'a 0))
(dict-values *a*) ; => (list 0)
(dict-clone a)
The (mset .. )
(short for mathematical set) corresponds to JavaScript's Set
structures.
(mset 0 1 1)
(defconstant *a* (mset 0 1)
*b* (mset 1))
;; Performs a pure/non-destructive merge, returns a
;; new mset with *a* and *b* merged into
(mset-merge *a* *b*)
;; Performs an unpure/destructive merge, alters
;; the *a* constant
(mset-merge! *a* *b*)
(defconstant *a* (mset 0 1 1))
(mset-pairs *a*) ; => (list (list 0 1))
(defconstant *a* (mset 0 1 1))
(mset-values *a*) ; => (list 0 1)
(mset-clone a)
Regular expressions can be created with the (regex .. )
macro. It is necessary to double escape backslashes:
(defvar regex-foo-call (regex "\\(foo\\)" 'g))
(regex-clone a)
Generators are available in Sibilisp with the (generator)
macro. For yielding, the (yields)
and (yields-all)
macros have to be used. To return a final value, utilize the (return)
macro.
Remember that generators use statements, so you cannot use the ususal
(if)
,(while)
or(for)
macros inside a generator!
(generator range-ints (start stop step)
(defvar n (ternary (number? start) start 0)
m (ternary (number? stop) stop 100)
i (ternary (number? step) step 1))
(while* (< n m)
(yields n)
(incr-by n i))
(return m))
(generator? range-ints)
Sibilisp provides special macros for creating and working with Promise
s. In Sibilisp, promises are named future
to represent that they contain a value in the future.
Usually, a future
is created with the equally named macro:
(future (resolve reject)
(if (user-is-logged-in?)
(resolve (get-user-data))
(reject (error "You have to be logged in!"))))
(future-resolve any-value)
(future-reject (error "failure"))
(future-all ...(.map values future-returning-function))
-
(future-any ...(.map values future-returning-function))
.
Complementing Sibilant's pipe
or |>
macro, Sibilisp provides the pipe-right
or <|
macro.
(<| (list 1 2 3)
(as-boolean)
(.find (lambda (n)
(eql? 0 (% n 2)))))
Conditional macros are used for control flow in a program. The following list gives a brief overview about all conditional macros available in Sibilisp.
(ternary (eql? a b) "Yep" "Nope") ; ((a === b) ? "Yep" : "Nope")
(cond ((eql? a b) "a equals b")
((eql? a c) "a equals c")
:else "a does not equal b or c")
(when (eql? a b)
"a equals b")
(if (eql? a b)
"a equals b"
"a does not equal b")
(typecase a (:string "value is a string")
(:number "value is a number")
(:function "value is a function")
(:hash "value is an object")
(:else "got something else..."))
The idiomatic way to create local variables in Sibilisp is via the (let .. )
and (let* .. )
macros.
They differ in how variables are bound: Variables defined with (let .. )
cannot reference other variables
of the same let statement, while variables inside a (let* .. )
can.
(let ((a 5)
(b 10))
(+ a b))
(let* ((a 5)
(b (* a 2)))
(+ a b))
Besides these, Sibilisp has a (defconstant .. )
macro to define constants as well as (defvar .. )
for global, mutable bindinings:
(defconstant **URL "https://api.acme.com/news")
(defvar *a* 5)
TIP
The
(defconstant ..)
and(defvar .. )
macros can be used to define multiple values at once. For example:(defconstant *MIN_VALUE* 0 *MAX_VALUE* 10)
Much like Common Lisp, Sibilisp supports generalized variable or property/index assignment via the (setf .. )
macro. For variable assignment, (setf .. )
only supports assigning one variable per call. If you want to assign multiple variables at once, use Sibilant's (assing .. )
macro.
(defvar *a* 10) ; let a = 10;
(setf *a* 20) ; a = 20;
(defvar *b* (hash :foo (hash :bar 10))) ; let b = { foo: { bar 10 } }
(setf *b* 'foo 'bar 20) ; b.foo.bar = 20
(defvar *c* (hash :foo (list (hash :bar 10)))) ; let c = { foo: [{ bar: 10 }] }
(setf *c* 'foo 0 'bar 20) ; c.foo[0].bar = 20
(defvar *d* (list (hash :foo (hash :bar 10)))) ; let d = [{ foo: { bar: 10 } }]
(let ((var-key 'foo) (var-index 0))
(setf *d* var-index var-key 'bar 20)) ; d[0].foo.bar = 20
(defvar *a* 10) ; let a = 10;
(assign *a* 20) ; a = 20;
To increment a value, the (incf)
macro can be used. It accepts an optional amount as second argument and falls back to 1
if none is supplied. Decrementation is possible via the (decf)
macro.
(incf *a*) ; a += 1
(incf *a* 4) ; a += 4
(decf *a*) ; a -= 1
(decf *a* 4) ; a -= 4
The generalized getter macro for data structures is (getf .. )
which can access either by key or index. To set a property/index in a structure, use (setf)
.
(getf window 'inner-width) ; window.innerWidth
(getf jquery-collection 0) ; jqueryCollection[0]
(getf (list 1 2 3) 2) ; [1, 2, 3][2]
(getf (list 1 2 (list 3 4)) 2 1) ; [1 2 [3 4]][2][1]
Using (with-fields .. )
also allows to get the fields of structures. Which one you use depends on preference:
(defconstant **USER (hash :id 1 :firstname "John" :lastname "Doe"))
(with-fields **USER (firstname lastname)
(.log console (+ "Hello " firstname " " lastname)))
Besides the (while .. )
macro from Sibilant, Sibilisp adds a (for .. )
macro to the language that
compiles into a JavaScript for-of
loop.
(for coll a
(.log console a))
(while (< i 10)
(.log console (incr i)))
Sibilisp enables the use of tail end recursive functions with the (loop)
and (recur)
macro combination. (loop)
declares a recursive block of code with the initial variables and (recur)
specifies the places where recursion occurs. Under the hood, the transpiler uses a technique called "trampolining" to create a set of nested function calls and a single while
loop out of the code it is fed with.
(defun fact-of (x)
(loop ((current (as-bigint 1))
(steps x))
(if (<= steps 0)
current
(recur (* current (as-bigint steps))
(decf steps)))))
(fact-of 5) ; => 120n
(fact-of 10000) ; => 28462596809...000n
Sibilant provides an (include .. )
macro, which allows to include the contents of a file
at compile time. It also has Node compatible (require .. )
and (export .. )
macros.
Sibilisp adds to this the (use .. )
, (use-all .. )
, (provide .. )
and (provide-all .. )
macros which map to the ES2015/ES6 module system. This allows to include Sibilisp into a toolchain
that uses compatible bundlers like Webpack or Rollup.
The following table gives you a brief overview:
Sibilisp | JavaScript ES2015+ |
---|---|
`(use "path/to/file" x) | import { _x_ } from "path/to/file.js"; |
(use "path/to/file" (_x_ :as _y_)) |
import { _x_ as _y_ } from "path/to/file.js"; |
(use-all "path/to/file" _x_) |
import _x_ from "path/to/file.js"; |
(use-all "path/to/file" :as _x_) |
import * as _x_ from "path/to/file.js"; |
`(provide x) | export _x_; |
(provide-all _x_) |
export default { _x_ } |
(provide-all (_x_ :as _y_)) |
export default { _y_: _x_ } |
Sibilisp allows creation of new tagged type constructors (TTC) via the (deftype .. )
macro. A TTC is a
type constructor much similiar to JavaScripts String
or Number
constructors in that you can use them without using
the new
operator. This allows to use a TTC like a regular function, for example in a mapping operation.
The "tagged" part means that the constructor uses the argument names to store the values of given arguments
to the created instance. For example, the addition
TTC we define below takes one argument and stores it
under the property number-value
.
(deftype addition (number-value)) ; also see the "methods" section
;;; Create a new addition
(defvar a (addition 1)
b (addition 2))
Type instances allow checking against via the (instance-of? .. )
macro.
Methods of TTC's can be defined using the (defmethod .. )
macro. This defines a method on the .prototype
of the type.
;;; make `addition` a semigroupoid
(defmethod addition concat (next-add)
(addition (+ (getf this 'number-value)
(getf next-add 'number-value))))
(pipe (list 1 2 3 4 5)
(.map addition) ; => map the constructor
(.reduce (lambda (a b) (.concat a b))) ; => fold & concat
(getf 'number-value)) ; => 15
To add a static method to a TTC, add the :static
keyword:
(defmethod addition of :static (value)
(addition value))
A tagged sum type constructor can be constructed with the (defsum .. )
macro.
(defsum color ((hsl hue saturation lightness)
(rgb red green blue)
(hex value)))
Instances of sum types have a .match
method that allows basic pattern matching against the type:
;; reusing the color sum type defined above
(defconstant *main-color* (color.hsl 4 80 50))
(defun show-hsl (h s l)
(+ "hsl(" h ", " s "%, " l "%)"))
(defun show-rgb (r g b)
(+ "rgb(" r ", " g ", " b ")"))
(defun show-hex (h)
(+ "#" h));
(defun show-color (color)
(.match color (hash :hsl show-hsl
:rgb show-rgb
:hex show-hex)))
(show-color *main-color*) ; => "hsl(4, 80%, 50%)"
A more idiomatic and concise version uses the (match-sum)
macro (available since version 0.6.8):
;; reusing the color sum type defined above
(defconstant *main-color* (color.hsl 4 80 50))
(defun show-color-2 (color)
(match-sum color ((:hsl (h s l) (+ "hsl(" h ", " s "%, " l "%)"))
(:rgb (r g b) (+ "rgb(" r ", " g ", " b ")"))
(:hex (h) (+ "#" h)))))
(show-color *main-color*) ; => "hsl(4, 80%, 50%)"