-
Notifications
You must be signed in to change notification settings - Fork 72
The Power of the TokenMap
TokenMaps are simple and yet powerful structures. This page is an advanced guide on what a TokenMap can do. To see the basic features of the TokenMap (required to understand this page) make sure to read the Basic Containers page before this one.
The TokenMap class is the core key-value container used by the calculator class and it has an interesting property: Every TokenMap has a pointer to a parent TokenMap. Making it an hierarchy, where only the ones on the top have no parent.
The default TokenMap constructor will set TokenMap::base_map() as its parent. While the default GlobalScope (which is a subclass of TokenMap) constructor will set TokenMap::default_global() as its parent, and that is the only difference between these 2 classes.
If you want to add some function, or attribute to all TokenMaps, or to all GlobalScopes, just add it to their parent TokenMap, so it will be accessible to all of them.
For those used to programming in JavaScript this concept is easy to understand, the parent TokenMap is the prototype of that map as in JavaScript Objects.
For those not used to JavaScript, having a parent map means that if I try to search for a key "my_key" on a map that doesn't have this item, it will first search on itself, fail, and then search on the parent map, repeating the process until there is no more parent maps to search. To better illustrate this concept:
TokenMap Top;
Top["name"] = "top";
Top["x"] = 10;
TokenMap Middle = Top.getChild();
Middle["name"] = "middle";
Middle["y"] = 20;
TokenMap Bottom = Middle.getChild();
Bottom["name"] = "bottom";
Bottom["z"] = 30;
// The function find() is the one responsible for searching on
// the entire hierarchy, note that it will return a NULL pointer
// if the key was not found.
cout << Bottom.find("name")->str() << ", "
<< Bottom.find("x")->str() << ", "
<< Bottom.find("y")->str() << ", "
<< Bottom.find("z")->str() << endl;
/* Result:
* bottom, 10, 20, 30
*/It is also important to notice that setting values on child objects won't affect the parent objects attributes, instead it will only hide them behind a more local definition. e.g.:
Bottom["x"] = 1;
Bottom["y"] = 2;
cout << Top.find("x")->str() << " vs " << Bottom.find("x")->str() << endl;
cout << Middle.find("y")->str() << " vs " << Bottom.find("y")->str() << endl;
/* Result:
* 10 vs 1
* 20 vs 2
*/On these examples, its easy to see that a prototype hierarchy has some resemblance with class inheritance, with the following differences:
- There are no constructors
- No private attributes
- And that parent attributes and functions might be changed or added at any time
Since the namespace of every expression evaluation is given by a TokenMap it is easy to manage more local scopes as child scopes, and more global scopes as parent scopes.
For instance when you declare a GlobalScope instance, its parent will be the TokenMap::default_global() scope. On this scope it is written all the builtin functions and variables, so the final user can access them.
Now there is a tricky part: Inside an expression maps work much like they do on C++ code, except in the corner situation where you:
- First declare a variable on a parent scope
- And then make an assignment on child scope using the same name of that variable.
e.g.:
TokenMap Global;
TokenMap Local = Global.getChild();
calculator::calculate("x = 10", Global);
calculator::calculate("x = -10", Local);In this particular case, when you define a variable on the parent scope, then make an assignment to it on the most local scope, the original value will be overwritten by the new one. This is the default behavior on most programming languages. The only way to make sure you won't overwrite an externally declared variable is to explicit declare a local variable before making the assignment.
Having said this, there is a builtin protection for variables on the TokenMap::default_global() scope. Since most users will never know what variables exist in the default global scope, allowing them to be overwritten by accident would be a liability. For that reason, the calculator explicitly check if the referred variable on the assignment belongs to the default global scope, and, in that case only, it will not overwrite the original value, but instead create a local variable.
This behavior may seem overly complicated, but if it was any different it would certainly cause unpleasant surprises for our users. You now might be thinking: "What if the final user wants to change a variable on the global scope?", well there are 2 easy ways to allow that:
- You might add a key-word
globalto the global scope, e.g.:
TokenMap::default_global()["global"] = TokenMap::default_global();This way an user could explicitly overwrite or even erase any variable he wants on the global scope accessing it as a normal TokenMap:
GlobalScope vars;
calculator::calculate("global.exp = None", vars);
calculator::calculate("global.pop('abs')", vars);-
The second solution is when you don't really want to overwrite the default_global scope, but to be able to change values of global variables you declared your self, in this case there isn't a real problem. You just need to have a second global scope whose parent is the default global scope. The child one will not be protected and any variables declared there can be accessed implicitly as you would expect.
GlobalScope my_global /* whose parent is the default global */; TokenMap my_local = my_global.getChild(); calculator::calculate("x = 10", my_global); calculator::calculate("x = 20", my_local); cout << my_global["x"] << endl; // 20
The final user will see no difference, and will not be exposed to the risk of overwriting default global variables by accident.
Prototypes are very useful. They allow a parent object to define a set of functions and attributes that will be accessible to all its child objects. But as JavaScript users know, they can do much more.
There are a few built-in features for supporting prototypical inheritance, for instance all functions receive among its arguments a token named this, allowing them to make changes on the TokenMap that contains them. The example below shows how to create a Triangle object, that knows how to calculate its own area:
// The function to calculate the area of a triangle:
packToken triangle_area(TokenMap scope) {
return scope["this"]["height"] * scope["this"]["width"] / 2;
}
// A Triangle Constructor, expects 2 args: "width" and "height":
packToken new_triangle(TokenMap scope) {
TokenMap triangle = scope["this"].getChild();
triangle["width"] = scope["width"];
triangle["height"] = scope["height"];
return triangle;
}
int main() {
GlobalScope scope;
scope["Triangle"] = TokenMap();
scope["Triangle"]["area"] = CppFunction(&triangle_area, "area");
scope["Triangle"]["new"] = CppFunction(&new_triangle, {"width", "height"}, "");
calculator::calculate("T = Triangle.new(10, 8)", scope);
std::cout << calculator::calculate("T.area()") << std::endl; // 40
}Also note there is a built-in function named extend that will return a new TokenMap whose parent is the map received as argument e.g.:
calculator::calculate("parent = {}");
calculator::calculate("child = extend(parent)");This function might be useful for using as base to create your own class system.