Skip to content

littlehaker/j-expression

Repository files navigation

j-expression

S-expression but in JSON

S-expression

S-expression is also known as 'symbolic expressions' are a type of notation used to represent structured data, which is commonly used in LISP.

Typical forms of S-expression are like below:

;; Strings can be added
(append "Hello " "world!") ; => "Hello world!"

;; If you want to create a literal list of data, use ' to stop it from
;; being evaluated
'(+ 1 2) ; => (quote (+ 1 2)) => (+ 1 2)

;; Now, some arithmetic operations
(+ 1 1)  ; => 2
(- 8 1)  ; => 7

;; `list' is a convenience variadic constructor for lists
(list 1 2 3) ; => '(1 2 3)

As we can see, S-expression is all about list. LISP interpreter evals the list to get the result.

JSON

What's the alternative of list? JSON! We can use JSON to represent S-expression.

// String startsWith "$" like "$append" is a symbol live in the Environment, otherwise "Hello " is a string.
// String startsWith "$$" will escape this rule.
["$append", "Hello ", "world!"] // => (append "Hello " "world!") => "Hello world!"
["$append", "$$Hello ", "world!"] // => (append "$Hello " "world!") => "$Hello world!"

["$quote", ["$add", 1, 2]] // => (quote (+ 1 2)) => '(+ 1 2)

["$add", 1, 1] // =>  (+ 1 1)   ["$add", 1, 1] makes more sense than ["$+", 1, 1]
["$subtract", 8, 1] // => (- 8 1)

["$list", 1, 2, 3] // => (list 1 2 3) => '(1 2 3)

And j-expression is a simple interpreter to eval these expressions.

Scenario

This kind of expression can be used for dynamic rules.

We can provide a UI/Editor to generate such expression, then eval it to get the return value.

Please check the demo(https://j-expression-editor.vercel.app) and the source code(https://github.com/littlehaker/j-expression-editor) for a simple editor.

screenshot

Installation

npm i j-expression

Usage

import JExpression from "j-expression";
const expr = new JExpression();

expr.eval(["$add", 1, 2]); // => 3

// Custom Environment
expr.define("answer", 42);
expr.eval("$answer"); // => 42

Environment

Environment is where the interpreter accesses the symbols.

  • Comparison: $gt / $lt / $eq
["$gt", 2, 1] // => true
["$lt", 2, 1] // => false
["$eq", 2, 1] // => false
  • Computation: $add / $subtract / $multiply / $divide
["$add", 2, 1] // => 3
["$subtract", 2, 1] // => 1
["$multiply", 2, 2] // => 4
["$divide", 2, 1] // => 2
  • Condition: $if / $cond
["$if", true, "foo", "bar"] // => "foo"
["$cond",
  [["$gt", 2, 1], "foo"],
  [["$lt", 2, 3], "bar"],
  [true, "baz"]] // => if 2 > 1 then "foo" else if 2 < 3 then "bar" else baz => "foo"
  • Boolean: $and / $or / $not
["$and", true, false] // => false
["$or", true, false] // => true
["$not", true] // => false
  • List: $list
["$list", 1, 2, 3] // => [1, 2, 3]
  • Definition: $def
["$def", "$answer", 42]
"$answer" // => 42
  • Function: $fn
[
  ["$fn", ["$val"], ["$add", "$val", 1]], // define a function with $val as parameter
  3
] // => 4
  • Multi Expression: $do
["$do",
  ["$def", "$inc", ["$fn", ["$val"], ["$add", "$val", 1]]],
  ["$inc", 3]
] // => 4
  • Eval: $quote / $eval
["$quote", ["$add", 1, 2]] // => ["$add", 1, 2]
["$eval", ["$quote", ["$add", 1, 2]]] // => 3

Lexical Scope

You can use let binding to create lexical scopes.

["let", ["$a", 1, "$b", 2], // => let a = 1, b = 2
  ["$add", "$a", "$b"]] // => ["$add", 1, 2] => 3

Also, you can create nested lexical scopes.

["$let", ["$a", 1], // => let a = 1
  ["$let", ["$b", 2], // => let b = 2
    ["$add", "$a", "$b"]]] // => ["$add", 1, 2] => 3

Asynchronous

You can use evalAsync to do asynchronous evaluation.

expr.define("addAsync", async (a, b) => a + b);
expr.define("deferredValue", Promise.resolve(42));

await expr.evalAsync(["$addAsync", 1, "$deferredValue"]) // => 43

License

MIT

About

S-expression but in JSON

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published