© 2019 Meouzer Consortium

JavaScript: Copying Functions with Evaluators Part 1

By draft 20 we will get it right! That will be 100 cat years.

Meouzer the Snarky Cat Programming Cat Making Eval Great Again! meouzer@gmail.com
loading
I rollick in bubble wrap because bubble wrap is always in context, and context is everything.
Meouzer

Meouzer is currently busy working in context!

Oh! By the way, eval will be used a lot, so let's end the meowsense about eval being evil and make it great again. Saying eval is evil is like saying hammers are evil because Dr. Evil could use one to boink yourself in the noggin. So you use eval on a string that doesn't belong to you and you end up with covid. Yea! Well don't do that.

Full code is at evaluators.js, which has no dependencies.

Introduction

You can skip the introduction for now because it is a high level summary of function copying that you probably won't understand at first. Read it when you want a review, after understanding the basics in the rest of the article.

Evaluator
An evaluator is a function, that takes a function and uses eval on it to define and return a copy of that function.

The context of the evaluator is the outer context of every function copy returned by the evaluator. This is why we say functions are copied into the evaluators context, or that function copies live in the evaluator's context.
Parent Evaluator
A parent evaluator is a function that defines and returns an evaluator, while having the the necessary scope to provide the evaluator with the outer context that it needs to copy functions.
Context Evaluation Factory
A context evaluation factory is a function that takes an input parameter called the context object. It then writes out out a parent evaluator using eval so that the parent evaluator has a scope that is specified by the context object.
Copying Functions starting with a CEF
What happens is that a call to the CEF calls the parent evaluator to get an evaluator, which is returned from the CEF. The evaluator, is then used to copy functions into its context.

So that the CEF and evaluator do not introduce unwanted outer context for the copies of functions that the evaluator produces, the CEF and evaluator should have empty scope. Also to avoid unwanted context, the evaluator and parent evaluator should be defined on return statements so that they are nameless.

As you see, evaluation/copying of functions is all about scope and context. Thus we have a Scope and Context section to correct common misunderstandings of these terms. Those who incorrectly believe that Scope is all that is visible, or don't understand the difference between scope and context won't be able to understand the article.

Show Me the High Level Code and Explain it Later

Global and Local Context Evaluation Factories
// The global context evaluation factory const ceFactory = function() { return eval("(function(" + Object.keys(arguments[0]) + "\c)\ {\ return function(){return eval('('+arguments[0]+')');}\ }).apply(null,Object.keyValues(arguments[0]));"); } // Line 2-4 does a lot. It writes out the text of a parent evaluator, // eval then defines the parent evaluator, whereupon the PE is // immediately called to return the evaluator on line 3. // To create local context evaluation factories // use eval on the following string locally. const cefString = '('+ceFactory +')';
// Utility function Object.defineProperty(Object, 'keyValues', { value:function(X) { return Object.keys(X).map(function(x){return X[x];}); } });
Example 1: Use ceFactory, the Global Context Evaluation Factory
var Z = 100; // global variable written in the global scope (function(){ // In some scope A, not the global scope // This local Z, and X are completely ignored by evaluator // copies because the factory ceFactory is global. var Z = 0; var X = 1; const evaluator = ceFactory({b:2, c:3}); // {b:2, c:3} is the context object const sum = evaluator(function(a){return a + b + c + Z}); alert(sum(1)); // 1 + 2 + 3 + 100 = 106 })();
Example 2: Use a Local Context Evaluation Factory
var Z = 100; // global variable (function(){ // In some scope A, not the global scope // This local Z, and X are not ignored by evaluator // copies because the factory is written at this scope. var Z = 0; var X = 1; const localFactory = eval(cefString); const evaluator = localFactory({b:2, c:3}); const sum = evaluator(function(a){return a + b + c + Z}); alert(sum(1)); // 1 + 2 + 3 + 0 = 6 })();

The rest of the article will show how ceFactory and cefString arose.

Scope and Context

We take our definitions of scope and context from careful readings of Wikipedia: Scope and Scope Chain and Activation Objects , taking their nuances into account. Multiple authors put a lot of thought into the concepts of scope and context. We do not allow multiple meanings for either term and insist on precision: to do otherwise is simply wishy-washy.

The reader is warned that the view that scope is all that is visible, is very wrong. It's wrong because it makes scope ambiguous and does not leave a separate and precise meaning for context. We insist on non ambiguous precise language that covers all the important concepts that actually occur in copying functions with evaluators.

Global Scope
Global scope consists of the variables (functions are variables) that are declared outside of every function.
Function Scope
A function's scope consists of the variables (including the parameters) declared by the function: It's understood the variables declared by a function are declared outside of any nested function.
Scope
Global Scope or Function Scope
Scope Chain
The scope chain of a function is a sequence of scopes. The first element is the function's scope. The second element is the scope of the function's parent. This process of entering scopes of parent functions into the sequence continues until the global scope has been entered into the sequence.
Function Context
A function's context is the set of all variables in the function's scope chain. However, there is one caveat. A variable in a higher scope whose name duplicates the name of a variable in a lower scope is hidden, and is not part of the function's context.

We say that a function inherits the context of its parent, but can put new scope (the fuction's scope) into context, which might possibly override the parent context.
Function Outer Context
A function's outer context is the context of the function sans its scope. Equivalently it is the context of the function's parent function sans hidden variables in the parent.
Location Context
A particuar point/location in code has a context. It is simply the context of the innermost function containing it. It is also the set of all variables visible at that point in code.

Test Your Understanding

The following questions are not rhetorical as they pop up naturlly in copying functions.

  1. Are the global scope and the global context the same?
    • Yes!
  2. Can two different scopes overlap?
    • No! Never!
  3. Can two different contexts overlap?
    • Yes they can. Sans hidden variables, the global scope is in every context. Generally, sans hidden variables, a function that is ancestor to two functions has scope that belongs to the context of both functions.
  4. What is most closely related to visibility of variables? Scope or Context?
    • Context!
      • Let's hammer this one home.
      • There are more variables visible to a function than its scope.
      • Scope is function scope or global scope. Scope is not everything that is visible from a particular location: In fact the following quote gives good reason for explicitly disallowing such a meaning.
        • The term "scope" is also used to refer to the set of all entities that are visible or names that are valid within a portion of the program or at a given point in a program, and which is more correctly referred to as context or environment.
          Wikipedia
  5. When is the context of a function the same as its parent?
    • When the function has empty scope!
  6. When is the context of a function the same as the context of its location?
    • When the function has empty scope!
  7. True or False. The outer context of a function is the location context of its definition.
    • True

How to Copy a Function

Copying a function means to create another function with the same text. Both source and target of the copy will have equivalent scopes. However, their outer contexts are dictated by their locations, or points of definition, whence the outer contexts generally differ.

To copy a function, you stringify it and then use eval on the resulting string. Stringify means that you obtain a string by surrounding the function with parentheses.

Example - Copy a function with eval
// In some scope A var z = 1; const sum = function(a,b){return a + b + z;} const sumString = "(" + sum + ")"; (function() { // Inside some scope B. var z = 10; const sumCopy = eval(sumString); // Line 6 is, via eval expansion, the same as writing // const sumCopy = function(a,b){return a + b + z;} in place // on the same line 6. alert(sumCopy(1,2)); // = 1 + 2 + 10 = 13 })();

sumCopy is a reference to a function defined on the right side of line 6. We will think of sumCopy as a function except in technical situations where it's important to realize that sumCopy is just a reference to a function. Keep in mind that the context/outer-context of the function is not determined by where the reference is defined, but by where the function is defined. In this case they're the same, but it will get wild later on.

Evaluators

An evaluator is a function that takes a function as its single argument, copies the function, and returns the copy.

Example - Copy a function with an evaluator
// define an evaluator const evaluator = function(func){return eval('(' + func + ')');} // copy an anonymous function // sum is the copy of function(a, b){return a + b;} const sum = evaluator(function(a, b){return a + b;}); alert(sum(1,2)); // 1 + 2 = 3

Now imagine on line 1 that the text of func is expanded in place to produce the copy, because that's what actually happens. Thus the copy is nested inside the evaluator, i.e., the parent of the copy is the evaluator. The copy therefore inherits the context of the evaluator. In particular, the variable func as a member of the context of the evaluator is also a member of the context of the copy. Even though the copy doesn't use func, func is still in context, i.e., it's still available for use by the copy. So let's actually see the copy use func in the following listing.

Example - A copy inherits the evaluator's context
// define an evaluator. const evaluator = function(func){return eval('(' + func + ')');} // copy an anonymous function that uses func const sum = evaluator(function(a, b){alert(func); return a + b;}); alert(sum(1,2)); // alerts "function(a, b){alert(func); return a + b;}" before alerting 3.

So this is BAD! Forcing the variable func into the context of the copy that the copy might accidently use is bad. Forcing func into the context of the copy that might override another variable func of the same name that the copy actually wants to use is bad.

So what we just learned is that an evaluator should introduce no context of its own, i.e., an evaluator should have empty scope. Yes! Its easy to write an evaluator with empty scope. The evaluator simply doesn't declare any parameters (or other variables).

Example - An evaluator with empty scope
// define an evaluator with empty scope. // arguments[0] is the function to be copied. const evaluator = function(){return eval('(' + arguments[0] + ')');} const sum = evaluator(function(a, b){return a + b;}); alert(sum(1,2)); // alerts 3

Above sum defined on line 2 is a reference to a function defined on line 1 while evaluator executed. While we will still call sum a function copy, keep in mind that all function copies are actually references to the true copies defined at at the location where eval creates it.

Providing Context to Function Copies

In Meouzer's magical evaluator paradigm of function copying using eval to expand function text in place to accomplish amazing things, it's the parent of the evaluator whose scope provides needed context to the copies created by the evaluator.

We want to copy the following three functions to a particular location and supply the needed context variables a and b.

  1. function(value) { a = value; }
  2. function() {return a; }
  3. function() {return a * b; }
Example - Providing Copies with Context
// getEvaluator is the parent to the evaluator returned on line 4. Its // scope provides context to the copy functions returned by the evaluator. // In some scope A function getEvaluator() { // set up some context for function copy on line 4 var a = 1; var b = 2; // return an evaluator return function(){return eval('('+arguments[0]+')');} } (function() { // In some scope B thematically miles apart from scope A const evaluator = getEvaluator(); // copy three functions into the evaluator's context const setA = evaluator(function(value){a = value;}); const getA = evaluator(function(){return a;}); const product = evaluator(function(){return a * b;}); // Make sure the variables declared in the evaluator parent // are in context of the functions that evaluator copied. var a = -100; // but try to confuse the evaluator and its copies var b = -200; setA(100); alert(getA()); // 100 alert(product()); // a*b = 100 * 2 = 200 })();

The evaluator parent needs to be careful with its scope. If for example, after line 3, the declaration var helperVariable = { }; is made, then helperVariable is in the context of all function copies, setA(), getA(), and product(). As noted before, that would be BAD!

Of course, colloquially speaking setA, getA, and product are functions. However, with the utmost technical precision in mind, are setA, getA, and product really functions? The answer is no because they are all references to functions, each of which was defined on line 4 at return statement. Why is this nuanced distinction important? It's important because if you want to determine the outer-context of setA, getA, and product you have to look at line 4 where they are defined. You will find that they all have the same outer context. Looking at lines 7, 8, or 9 tells you nothing about their outer context. So the variables on lines 2 and 3 are in their outer context, but the variables on lines 9 and 10 are not in their outer context.

Context Evaluation Factories

The context object is a literal object. The names of its keys are the names of the variables that are to be put into context for the function copies. The key values are the values for the corresponding variables. For example with the context object {a:1, b:new Date()}, the variables var a = 1, and var b = new Date() are forced into the context of the function copies created by the evaluator.

  1. Summation of Contexts
  2. The outer-context of a function copy created by the evaluator is always the evaluator's context.
    • That's why we can say an evaluator copies functions into its context.
  3. The evaluator's context is the factory's context but overridden as directed by the context object.
  4. The factory's context is the context of the factory's location because the factory has empty scope.

The Globally Scoped Context Evaluation Factory

The Globally Scoped Context Evaluation Factory
// Written in the global scope var Z = 100; // global variable // Context evaluation factory written at the global scope const ceFactory = function() { // The context object is arguments[0]. Remember we can't use // variables. return eval("(function(" + Object.keys(arguments[0]) + "\c)\ {\ return function(){return eval('('+arguments[0]+')');}\ }).apply(null,Object.keyValues(arguments[0]));"); } // The context of the context evaluation factory is forced into // the context of any evaluator copy. In particular, the global // variable Z = 100 will be forced into the context of any evaluator // copy. (function(){ // In some scope A, not the global scope // This local Z, and X are completely ignored by evaluator // copies because the factory is written at global scope. var Z = 0; var X = 1; const evaluator = ceFactory({b:2, c:3}); const sum = evaluator(function(a){return a + b + c + Z}); alert(sum(1)); // 1 + 2 + 3 + 100 = 106 })();
Helper Function Object.keyValues
\numbersOff Object.defineProperty(Object, 'keyValues', { value:function(X) { return Object.keys(X).map(function(x){return X[x];}); } });

Locally Scoped Context Evaluation Factories

The one and only globally scoped context evaluation factory might do exactly what you want, and might not. In fact you probably want a local, not global, scope forced into the outer context of evaluator function copies. No problem! You just rewrite ceFactory at the local scope in question. Of course that's just copying ceFactory and we know that can be accomplished be using eval on the stringification const cefString = '(' + ceFactory + ')'.

Context Evaluation Factory String
// At the glolbal scope ready for deployment in any scope const cefString = '(' + ceFactory + ')';
Locally Scoped Context Evaluation Factory
var Z = 100; // global variable (function(){ // In some scope A, not the global scope // This local Z, and X are not ignored by evaluator // copies because the factory is written at this scope. var Z = 0; var X = 1; const localFactory = eval(cefString); const evaluator = localFactory({b:2, c:3}); const sum = evaluator(function(a){return a + b + c + Z}); alert(sum(1)); // 1 + 2 + 3 + 0 = 6 })(); // Even if lines 6-8 were spread out in different scopes, // the scope A variables var Z = 0, and var X = 1 are // forced into the context of function copies. Why? // Because the code const evaluator = localFactory({b:2, c:3}); // does not define the evaluator!! This only obtains a reference // to the evaluator, which is actually defined on line 5. // Do you believe that sum is a reference to a function that is // also defined on line 5. You should because it's true. // Eval magic!

Evaluation Modules

The set of all function references created by an evaluator is called an evaluation module. The members of the module can work harmoniously using the same outer context, i.e., the context of the evaluator. If one member changes the value of a variable in the evaluator's context, the other members see the change: this is true even if the function references themselves are scattered across wildly different scopes (remember the actual functions which are referenced live in the evaluators context). One member can use another member if the latter is visible to the former.

// Create an evaluator with a = 1, and b = 2 in context. const evaluator = (function(a, b) { return function(){return eval('(' + arguments[0] + ')');}; }).call(null, 1, 2); // create three members of an evaluation module. // some of which use the others. const sum = evaluator(function(){return a + b;}); const scale = evaluator(function(f){a *= f; b *= f;}); const average = evaluator(function(){return sum()/2;}); // test it out scale(10); alert(sum()); // 10 + 20 = 30 alert(average()); // (10 + 20)/2 = 15