Previously, the AST node creation functions were discussed, in which a set of basic functions where defined which take varying amounts of TreeNode
inputs and return a specially typed TreeNode
sub-type output. These functions directly create POJO realizations of the typings described in TypeScript. In a standard, object-oriented approach to developing an AST, evaluation of these nodes would be either baked into a per-object-class evaluate
method, or offloaded and centralized to a series of (potentially interconnected) Visitor
patterns.
Recalling that part of the spec for this AST series is that "[...]these expressions [should] be capable of being evaluated to a singular numeric value[...]", it is insufficient to simply encode a series of nested operations as a tree and return that to whatever operational context made the request; these trees need to be evaluated! (In addition, again to spec, they need to be algebraically simplified, where appropriate.)
As a concession to the limited capability of TypeScript to describe algebraic data types (ADTs) after transpilation, POJOs (or Plain Old JavaScript Objects) are used to specify structured data. However, these objects are used purely for structural purposes: their underlying prototypes—what other languages might consider their class or even static/meta class—are not altered nor augmented. All operations performed on these typed POJOs is performed functionally.
This suggests that providing an evaluate
method—certainly feasible in OOP-flavored TypeScript—is out, as doing so breaks the POJO prototype. Visitors could be used, but are typically implemented via objects, so would again break one of the underlying desires of writing this system: functional design.
So, why not provide an evaluate
function, which takes an AST TreeNode
and, through use of recursion, figures out what the result of the operations so encoded is? Recall that the spec mentions variables, and, as a general principle, the idea of algebraic/syntactic simplification. This suggests that structured rules for different configurations of sub-trees would need to be considered to make semantically equivalent substitutions. And there are a distressingly large number of rules, especially for arithmetic operations. Bundling all of that in a single, centralized context is muddy.
While not perfectly ideal—as pure representation of unevaluated operations is not readily available, and interstitial sub-calculations are burdensome to isolate and consider—a reasonable tradeoff would be a sort of in situ evaluation, wherein each node creation function codifies rules for evaluating sub-trees interrelated to the operation being represented. That is, for example, the add
function would encode rules for simplifying trees whenever it is invoked, specifically looking at logic related to addition. Similarly for all other supported operations.
This is non-trivial to implement—again, there are many rules, and the exact space of all potential children a given node might have to separately consider can scale above P(n, c)
, where P
is the permutation function, and n
is the total number of TreeNode
sub-types that the system supports, and c
is the number of children a given node has. A given binary operation for a robustly sized system (at least, say, 40 different node types) could have well over P(40, 2) =~ 1500
different children permutations, each of which could have unique results.
Still, consider that most degenerate of cases: simple numeric evaluation. If all operations are solvable—even if evaluated to NaN
—as a numeric at the point of evaluation, that implies that the data leaf-nodes within the tree are, themselves, either numeric or semantically equivalent ot a numeric. In considering just the former, it becomes obvious that, at the very least, the previously defined creation functions must be augmented with codifications of the operations they represent. The attached source for this article presents one potential solution to providing that behavior, but other alternatives will be made more apparent as this series develops.
The next article in this series will be a slight break from ASTs to consider a methodology that can help develop the behavior this article would like to provide some solution to.