Skip to content

02 Language

David Hofmann edited this page Oct 22, 2024 · 32 revisions

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.


Contents


General information

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* ... )

Types

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")

Type checking

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)

Type coercion

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)

Errors

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")

Numbers

Because the target language is JavaScript, Sibilisp supports the same numeric data types Number and BigInt.

Comparison operators

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)

Arithmetics and math

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

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))

Function arguments

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

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

Date

To create dates, Sibilisp provides a (date), and some related macros.

(date 2020 0 1 18 5 57)              ; new Date(2020 ... )

Current time in UTC milliseconds

(defvar now-utc (date-now-utc))

Current time in local milliseconds

(defvar now (date-now))

Cloning a date

(defvar jan-1st-2020 (date 2020 0 1))

(date-clone jan-1st-2020)

Create a date from an ISO string

(defvar jan-1st-2020 (date-parse '2020-01-01'))

Note:
(date-parse) transpiles into JavaScript's Date.parse and therefor underlies some restrictions in what ISO strings can be passed in. An explanation can be found here: MDN

List

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)

Cloning a list

(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.

Hash

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;

Merging hash objects

(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*)

Get a key-value list from a hash

(defconstant *a* (hash :a 0))

(hash-pairs *a*) ; => (list (list a 0))

Get a key list from a hash

(defconstant *a* (hash :a 0))

(hash-keys *a*) ; => (list "a")

Get a value list from a hash

(defconstant *a* (hash :a 0))

(hash-values *a*) ; => (list 0)

Create a new hash from a base

(hash-create (getf instance 'prototype))       ; Object.create(instance.prototype)

(hash-create)                                  ; Object.create(null)

Clone a hash (shallow)

(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.

Dict

The (dict .. ) structure corresponds to the JavaScript Map structure.

Create a dict

(dict 'a 1 2 (gensym "b"))

Merging dict objects

(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*)

Get a key-value list from a dict

(defconstant *a* (dict 'a 0))

(dict-pairs *a*) ; => (list (list 'a 0))

Get a key list from a dict

(defconstant *a* (dict 'a 0))

(dict-keys *a*) ; => (list "a")

Get a value list from a dict

(defconstant *a* (dict 'a 0))

(dict-values *a*) ; => (list 0)

Clone a dict (shallow)

(dict-clone a)                                  

MSet

The (mset .. ) (short for mathematical set) corresponds to JavaScript's Set structures.

Create an mset

(mset 0 1 1)

Merging mset's

(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*)

Get a key-value list from an mset

(defconstant *a* (mset 0 1 1))

(mset-pairs *a*) ; => (list (list 0 1))

Get a value list from a mset

(defconstant *a* (mset 0 1 1))

(mset-values *a*) ; => (list 0 1)

Clone an mset (shallow)

(mset-clone a)                                  

Regex

Regular expressions can be created with the (regex .. ) macro. It is necessary to double escape backslashes:

(defvar regex-foo-call (regex "\\(foo\\)" 'g))

Clone a regex (clone has last index resetted)

(regex-clone a)                                  

Generator

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!

Example of creating 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 type-checking

(generator? range-ints)

Future

Sibilisp provides special macros for creating and working with Promises. 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!"))))
Other future related macros
  1. (future-resolve any-value)
  2. (future-reject (error "failure"))
  3. (future-all ...(.map values future-returning-function))
  4. (future-any ...(.map values future-returning-function)).

Language constructs

Flow control

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)))))

Conditionals

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 operator

(ternary (eql? a b) "Yep" "Nope")        ; ((a === b) ? "Yep" : "Nope")

cond

(cond ((eql? a b) "a equals b")
      ((eql? a c) "a equals c")
      :else "a does not equal b or c")

when

(when (eql? a b)
  "a equals b")

if

(if (eql? a b)
  "a equals b"
  "a does not equal b")

typecase

(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..."))

Variables and Constants

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

(let ((a 5) 
      (b 10))
  (+ a b))

let*

(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)

Assignment

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
assign
(defvar *a* 10) ; let a = 10;
(assign *a* 20) ; a = 20;

Incrementing/Decrementing

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

Getters

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

(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:

with-fields

(defconstant **USER (hash :id 1 :firstname "John" :lastname "Doe"))
(with-fields **USER (firstname lastname)
  (.log console (+ "Hello " firstname " " lastname)))

Loops

Besides the (while .. ) macro from Sibilant, Sibilisp adds a (for .. ) macro to the language that compiles into a JavaScript for-of loop.

for-of

(for coll a
  (.log console a))

while

(while (< i 10)
  (.log console (incr i)))

Recursion

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

Import/Export

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_ }

Custom types

Tagged type constructors

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.

Defining a new type

(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

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))

Tagged sum type constructors

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%)"

GitHub license NPM version Github stars NPM downloads