Emmy as JavaScript library (2)
(This post is the second part of a series, all with the same title)
Short intro
Sicmutils(JS) along with a parsimonious Scheme-to-JavaScript compiler is by comparison the most direct entry to the underlying SICMechanics book (also avalable in different formatting).
Here I start to explain the implementation of such a compiler.
A full version along with all 109 executed examples of the first part of the SICMechanics book is given on this Web page.
Let's start
I show you how to calculate rational numbers in JavaScript. I begin with a simple variable declaration
var x = 1;
and a simple calculation
x/2;
giving a floating point number. In case you are curious: I use Klipse for code-execution, you can change all code and see the result immediately.
Moving on, we note that we can always write any calculation as a function,
var div = (x, y) => x/y;
div(x, 2);
and whereas the notation becomes clumsier, the result remains the same floating point number. But we can do better and get to rational numbers if we import the right JavaScript library: Emmy1.
<script src="https://kloimhardt.github.io/blog/js/emmy.js/build/emmy_bundle.js"></script>
With the help of Emmy, we can redefine the calculation function
var div = emmy.div;
div(x, 2).toString();
and get the result as a rational number. But how to get back to the usual notation using the /
sign? For this we code the calculation as a string
var formula = "[ x / 2 ]";
and first, within the string, replace the /
var formula = "[ x / 2 ]";
var replaceMath = (txt) => txt.replace(/\//g,"div");
replaceMath(formula);
and second produce comma separated substrings. They do not look nice,
var formula = "[ x / 2 ]";
var insertCommas = (txt) =>
txt.replace(/(\w+)/g,'"$1",')
.replace(/\,\s+\]/g," ]");
insertCommas(replaceMath(formula))
but allow us to easily convert the original string into a JavaScript array.
var formula = "[ x / 2 ]";
var textToJson = (txt) =>
JSON.parse(insertCommas(replaceMath(txt)));
textToJson(formula);
This in turn allows for easy transformation into JavaScript code,
var formula = "[ x / 2 ]";
var jsonToJs = (j) =>
j[1] + "(" + j[0] + ", " + j[2] + ")";
jsonToJs(textToJson(formula));
which is a string again. This JS-code string can be evaluated. First, we put the above learnings into a function,
var expressionToJs = (expr) =>
expr[expr.length-1] === " "
? jsonToJs(textToJson(expr)) + ".toString();"
: "'add blank'";
expressionToJs(formula);
which imposes the condition that a conversion is only made when the string ends with a blank. Then we employ a small hack that kicks in this transpiler whenever some code in Klipse starts with a bracket -- square [
or round (
.
<script> var oldEval = window.eval; var newEval = (txt) => txt[0] === "[" || txt[0] === "(" ? oldEval(expressionToJs(txt)) : oldEval(txt); window.eval = newEval; </script>
VoilĂ , our first calculation of a rational number in JavaScript.
[ x / 2 ]
Next we implement a more complicated formula,
var formula = "((x / 2) / 2)";
which as a modification now uses round brackets ()
. Those have to be converted to square brackets.
var formula = "((x / 2) / 2)";
var makeBrackets = (txt) =>
txt.trim()
.replace(/\(/g,"[ ")
.replace(/\)/g," ],")
.replace(/,$/,"");
var textToJson = (txt) =>
// old: JSON.parse(insertCommas(replaceMath(txt)))
JSON.parse(insertCommas(replaceMath(makeBrackets(txt))));
textToJson(formula)
Since the formula is nested, we need a new function to transform a JS-array into JS-code.
var formula = "((x / 2) / 2)";
var jsonToJs = (j) =>
// old: j[1] + "(" + j[0] + ", " + j[2] + ")"
j.constructor == Array
? jsonToJs(j[0]) + "(" + j.slice(1).map(jsonToJs) + ")"
: j;
jsonToJs(textToJson(formula));
As you can see, we are not there yet, the symbols are somehow topsy-turvy, but we immediately notice that things work if we use prefix notation for our formula.
jsonToJs(textToJson("(/ (/ x 2) 2)"));
This gives us a hint how the flaw can be fixed: we write a helper function that swaps all first two elements in a nested array,
var swapFirst = (j) =>
j.constructor == Array
? [j[1], j[0]].concat(j.slice(2)).map(swapFirst)
: j;
swapFirst([["x", "div", 2], "div", 2]);
and add it to the transpiler function.
var formula = "((x / 2) / 2)";
var expressionToJs = (expr) =>
expr[expr.length-1] === " "
// old ? jsonToJs(textToJson(expr)) + ".toString();"
? jsonToJs(swapFirst(textToJson(expr))) + ".toString();"
: "'add blank'";
expressionToJs(formula);
Finally, let's give our formula a try.
((x / 2) / 2)
1: We used emmy.js and Browserify to create emmy_bundle.js