© 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
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
// 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];});
}
});
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
})();
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.
-
Are the global scope and the global context the same?
-
Can two different scopes overlap?
-
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.
-
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
-
When is the context of a function the same as its parent?
- When the function has empty scope!
-
When is the context of a function the same as the context of its location?
- When the function has empty scope!
-
True or False. The outer context of a function is the location context of its definition.
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.
// 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.
// 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.
// 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).
// 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.
- function(value) { a = value; }
- function() {return a; }
- function() {return a * b; }
// 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.
- Summation of Contexts
-
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.
-
The evaluator's context is the factory's context but overridden as directed by the context object.
-
The factory's context is the context of the factory's location because the factory has empty scope.
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 + ')'.
// At the glolbal scope ready for deployment in any scope
const cefString = '(' + ceFactory + ')';
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