When we last left off, we had a component called RhoExpressionComposer that stored information about what Rho function it represents as part of its state. This made it possible to prototype the expression composition interface, but unfortunately the information about the expression being composed was scattered and not accessible as a unified tree.
To fix this, we need to unify where the expression is stored to a single component. For prototyping, I used App as the root component which stored the expression in its state, and provided a setter callback that is provided to child components (either by passing it down as a prop, or the use of a context).
In order to allow the setter callback to update the correct part of the expression tree, we need to come up with a way to specify a kind of path up the tree. This path could simply be an array of integers that represent indexes. For example, the root REC component's path will be an empty array. This is something of a special case.
Consider what happens as we start with an empty expression and begin to build one. Let's build the expression say we want to create this simple expression: 1 + (2 + 3)
.
The root REC component will allow the user to select a Rho function. Say the user selects addition. The Rho function (addition) and the REC component's path get passed to the setter callback. It sees an empty path, and sets the root Rho function.
Now, the root REC component displays the addition function's composition interface. On the left and the right of the plus sign are REC components, which represent the first and second arguments that will be passed to the addition function. The code that displays this interface (the addition function's "render" method) tells its child REC components what their paths are -- in this case, [0] and [1].
Say the user then selects a "constant" for the right Rho function. The setter callback is executed, and again it's passed the desired Rho function, and the REC component's path -- in this case, [0].
The setter callback starts at the root Rho function (addition), and then sets its first argument (child 0, the index specified in the path) to the specified Rho function.
The value returned by the Rho constant function is simply its input value, so that any constant can be set. So, another call to the setter callback is made, with the path [0, 0], and the constant value 1. (This is handled by the Rho constant function's render method / composition interface. For this edge case the callback 'setFunctionFromPath' is slightly misnamed. This may be renamed setArgumentFromPath.)
Now on the right side of the root addition, the setter would be passed the path [1] and the Rho function addition.
The setter would then be called twice more: with path [1, 0], and with path [1, 1], but with the "constant" Rho function. Finally it would be called twice again: [1, 0, 0] and the constant value 2; [1, 1, 0] and the constant value 3.
One technical consideration I came across when writing the setter function was that each time any part of the expression tree got updated, the entire tree was re-created, in the sense that each node was cloned. Since each node is duplicated, the connections between them also need to be explicitly re-set.
Here is a snapshot of the code discussed above. Also see the render method of the Rho addition function in RhoFunctions.js for an example of how the path prop is modified and passed to child REC components.
App.js, RhoFunctions.js
In this way, the system can build up a unified expression tree. Next step will be to create a component that allows the creation of multiple lines of code, where each line is a Rho expression. Once that's done, it will make sense to add in a runtime; and start putting together the display where the turtle is moved about, and draws.