[Tech Note] Extensions

Discussions about extensions and frameworks

[Tech Note] Extensions

Postby oliver » Thu Dec 30, 2010 11:13 am

This tech note is about extending MorphEngine to add both new "built-in" functions and data types.
This is a quick overview.
You find detailed detailed information here http://naivedesign.com/ND1/ND1_Reference__JavaScript_API.html
and here http://naivedesign.com/ND1/ND1_Reference__Custom_Types.html.

Simple usage

There's a singleton object, "calculator", that you can talk to in JavaScript. You need just a single engine command, "push", to play with a fully-working RPN/RPL calculator.

Code: Select all

The top-most stack entry, at calculator.stack[0], will now hold the number 4.
Noteworthy architectural feature: the stack is a native JavaScript array. A JS array can hold any data type, which so perfectly mirrors the stack of an RPN calc, that latter can be former.

Another example
Code: Select all

At stack position 1 now resides a native array containing an integer, the name of a built-in constant, and an expression involving a user variable
Code: Select all

calculator.stack[0] is now ["(0,1)", 1.7724538509055159, "'sqrt(cos(a))'"]
(The user sees a prettier presentation.)
In analysis, the "sqrt" command was applied to each array element ("Parallel Processing" in HP-speak), yielding a result vector containing a complex number, a real, and a symbolic expression.

For a final example, a mini-RPL program
Code: Select all
  calculator.push("\<< -2 2 FOR I I \v/ NEXT 4 \->ARRAY \\>")

yields [(0,1), 0, 1, 1.4142135623730951] on the stack.

Adding a built-in function

A built-in function is a function property of a given name in an object that provides a function collection. Function collections are organized by data type.

For example, the implementation for the "sqrt" function are two lines of code in the following scaffolding:

Code: Select all
calculator = {
  functions: {
    "sqrt": function(x) { return (x >= 0 ? Math.sqrt(x) : this.complex.sqrt(x)); }
    complex: {
      "pow": ...
      "sqrt": function(c) { return this.pow(c, 0.5); }
      ..…   }
    vector: {
    function_aliases: {
     "\u221a": "sqrt"

This is the entire (!) implementation of the sqrt function in the engine. It permits to calculate with real- and complex-valued square roots on the stack, in RPL programs, in symbolic expressions, and in user-written JavaScript functions.
(The name mapping in 'function_aliases' is optional and makes the function known by another name. In this case, the Unicode for the square root sign.)

There's a number of mechanisms in place in MorphEngine to allow this level of automatism, but most are directly related to language features.
On the stack or in a symbolic expression, for example, MorphEngine notes the presence of a function when it encounters an unknown symbol. It employs the language's "in" operator to quickly determine if the unknown symbol is in a function collection. If so, it employs introspection to find out how many arguments it takes and provides the right number from the stack.
It does a tiny bit of code-morphing to extend the scope chain to make it available in user-written JavaScript as a valid function name.

Adding a new built-in function couldn't be easier.
You simply add to the function collection appropriate for your data type.

To add a real-valued cotangent function, you'd write
Code: Select all
  calculator.functions["COT"] = function(x) { return this.cos(x) / this.sin(x); }

and be done!

This makes COT work in all of these places:
on the stack: [3,4,5] COT (note, it will happily parallel-process a vector)
in an algebraic expression: 'COT(pi)'
in an RPL program: \<< pi COT \>>
in User JavaScript: function() { return COT(-4); }
in "natural math notation": f(x) = COT(x)*pi

In ND1 you can "inject" this line into the engine and have a permanent extension of your calculator.
You can do this entirely on the device. (No external tools or dev environments needed.)

Similarly, to add a built-in constant to the calculator and have it work calculator-wide (like 'pi', 'e' or 'i'), you'd simply do:
Code: Select all
  calculator.vars["c0"] = 299792458

If you know the name of a built-in function, you can overwrite or extend it.
For example the BigNum type extends the built-in factorial function like so:
Code: Select all
  calculator.functions["factorial_real"] = calculator.functions["factorial"]; // move built-in function to new place
  calculator.functions["factorial"] = function(x) { return (x>99 ? BigNum.factorial(BigNum.fromNumber(x)) : calculator.functions["factorial_real"].call(calculator.functions, x)); };

With that, input values greater than 99 will employ the BigNum version of factorial, while smaller values will continue to be processed by the pre-existing function.

Adding a data type

You can add an entirely new data type to the engine. It will work in all the places that a built-in data type works: on the stack, in UserRPL, etc.
It will work together with other built-in types: it can be an element in a vector or matrix, it will participate in parallel processing, etc.

The following is the entire code for a custom chemical formula data type.

Once injected into the engine, the user can type "H20" on the command-line and enter it as a chemical formula type on the stack. A little badge will appear beside the stack entry and subscripts will be used in the formatting of the formula for a nice presentation. The user can press the Eval key to see the formula's relative molar mass, and do a little math with other formulas instances using standard calculator keys: "+" and "*" suddenly work with chemical formulas!

Code: Select all
var ChemFormula = {
   type: "chem",
   isLoaded: true,

   toString: function(obj) { return obj.stringValue; },
   toHTML: function(obj) {
      var stringForType = this.toString(obj).replace(/[\*\[\]] /g, "").replace(/[1-9]+(?!\ )/g, "<sub>$&</sub>");
      return (stringForType + calculator.HTMLforTypeBadge(obj.type));
   isStringRepresentation: function(x) { return x.match(/[A-Z][a-z]?([1-9]+|([A-Z][a-z]?)|[\(\)])+/); },
   fromString: function(str) {
      function formulaObj() {
         this.type = ChemFormula.type;
         this.stringValue = str;
         this.toString = function() { return ChemFormula.toString(this); };
      return new formulaObj();
   "+": function(a, b) { // "symbolic" addition of formulas
//      if (typeof a === "object" && a.type == ChemFormula.type)
         return this.fromString(String(a) + " + " + String(b));
//      else
//         throw Error("bad arg");
   "*": function(a, b) { // "symbolic" multiplication of a quantity with a formula
      if (typeof a === "object" && a.type == ChemFormula.type) { var tmp = a; a = b; b = tmp; } // swap args, if 2nd arg is number
      return this.fromString(String(calculator.unquote(a)) + " * [ " + String(b) + "] ");
//      else
//         throw Error("bad arg");
   "Mr": function(obj) { // relative molar mass
      var atomicWeights = { // relative atomic mass, IUPAC 2007 data
         "H": 1.00794, "He": 4.002602, "Li": 6.941, "Be": 9.012182, "B": 10.811, "C": 12.0107, "N": 14.0067, "O": 15.9994,
         "F": 18.9984032, "Ne": 20.1797, "Na": 22.98976928, "Mg": 24.305, "Al": 26.9815386, "Si": 28.0855, "P": 30.973762, "S": 32.065,
         "Cl": 35.453, "Ar": 39.948, "K": 39.0983, "Ca": 40.078, "Sc": 44.955912, "Ti": 47.867, "V": 50.9415, "Cr": 51.9961,
         "Mn": 54.938045, "Fe": 55.845, "Co": 58.933195, "Ni": 58.6934, "Cu": 63.546, "Zn": 65.38, "Ga": 69.723, "Ge": 72.64,
         "As": 74.9216, "Se": 78.96, "Br": 79.904, "Kr": 83.798, "Rb": 85.4678, "Sr": 87.62, "Y": 88.90585, "Zr": 91.224,
         "Nb": 92.90638, "Mo": 95.96, "Tc": 98, "Ru": 101.07, "Rh": 102.9055, "Pd": 106.42, "Ag": 107.8682, "Cd": 112.411,
         "In": 114.818, "Sn": 118.71, "Sb": 121.76, "Te": 127.6, "I": 126.90447, "Xe": 131.293, "Cs": 132.9054519, "Ba": 137.327,
         "La": 138.9054519, "Ce": 140.116, "Pr": 140.90765, "Nd": 144.242, "Pm": 145, "Sm": 150.36, "Eu": 151.964, "Gd": 157.25,
         "Tb": 158.92535, "Dy": 162.5, "Ho": 164.93032, "Er": 167.259, "Tm": 168.93421, "Yb": 173.054, "Lu": 174.9668, "Hf": 178.49,
         "Ta": 180.94788, "W": 183.84, "Re": 186.207, "Os": 190.23, "Ir": 192.217, "Pt": 195.084, "Au": 196.966569, "Hg": 200.59,
         "Tl": 204.3833, "Pb": 207.2, "Bi": 208.9804, "Po": 209, "At": 210, "Rn": 222, "Fr": 223, "Ra": 226, "Ac": 227,
         "Th": 232.03806, "Pa": 231.03588, "U": 238.02891, "Np": 237, "Pu": 244, "Am": 243, "Cm": 247, "Bk": 247, "Cf": 251,
         "Es": 252, "Fm": 257, "Md": 258, "No": 259, "Lr": 262, "Rf": 265, "Db": 268, "Sg": 271, "Bh": 272, "Hs": 270,
         "Mt": 276, "Ds": 281, "Rg": 280, "Cn": 285, "Uut": 284, "Uuq": 289, "Uup": 288, "Uuh": 293, "Uuo": 294
      calculator.vars.local = atomicWeights; // make atomicWeight our new set of local vars
      var atomicWeight = calculator.eval(obj.stringValue.replace(/\[/g, "(").replace(/\]/g, ")").replace(/([A-Z][a-z]?)([0-9]+)/g, "$1*$2").replace(/([A-Z][a-z]?)|(\()/g, "+$&"));
      calculator.vars.local = {}; // reset local vars
      return atomicWeight;
   eval: function(obj) { return ChemFormula.Mr(obj); }

A data type extension like that can be written on a computer and imported, or directly on the device.
MorphEngine data type extensions include: Image, Color, Fractions, BigNum, URL, Code.
The Image extension permits screenshots to sit on the stack for example. Implemented in ~40 lines of code.

It's worth noting that actual JavaScript objects are sitting on the stack and are formatted for display according to code you write.
There're no imposed limits as to the nature of your data type. You could have a live (i.e. running) physics simulation sitting on your stack and have it participate with other objects.
Site Admin
Posts: 433
Joined: Sat May 01, 2010 2:11 pm

Return to Extensions

Who is online

Users browsing this forum: No registered users and 1 guest