Order of Operations

When looking at an expression like x + 10 * y, we know that the first thing to do is 10 * y and then add x, but it isn’t naturally obvious. Let’s look at why with a tree for x + 10 * y:

  
    graph TD;
        subgraph orderofop[x + 10 * y];
        add(+) --> x(x);
        add --> mul("×");
        mul --> 10(10);
        mul --> y(y);
        end

With the tree above, we can tell that we do the multiplication first, and the addition second. But if we use the symbolic notation without the tree, the most natural thing for people who are used to left-to-right reading is to solve it like (x + 10) * y. The order in which we have to solve it becomes clear when using the tree:

  
    graph LR;
        subgraph p1["(x + 10) * y"];
        
        mul("×") --> add(+);
        mul --> y(y);
        add --> x(x);
        add --> z10(10);
        end

        subgraph p2["x + (10 * y)"];

        add2(+) --> x2(x);
        add2 --> mul2(*);
        mul2 --> 10_2(10);
        mul2 --> y2(y);

        end

        p1 -.-> p2;

Properties

The properties of operations (commutation, association, distribution) are also easy to visualize with the tree:

Commutation

  
    graph LR;
        subgraph ab[a + b];

        add(+) --> a(a);
        add --> b(b);

        end

        subgraph ba[b + a];

        add2(+) --> b2(b)
        add2 --> a2(a)

        end

        ab -.-> ba;

Association

  
    graph LR;
        subgraph a_bc["a + (b + c)"];

        add(+) --> a(a);
        add(+) --> add2(+);
        add2 --> b(b);
        add2 --> c(c);

        end

        subgraph ab_c["(a + b) + c"];

        add3(+) --> add4(+);
        add3 --> c2(c);
        add4 --> a2(a);
        add4 --> b2(b);

        end

        a_bc -.-> ab_c;

Distribution

  
    graph LR;
        subgraph 2_xy["2 * (x + y)"];

        mul("×") --> 2(2);
        mul --> add(+);
        add --> x(x);
        add --> y(y);

        end

        subgraph 2x_2y["(2 * x) + (2 * y)"];

        add2(+) --> mul2(*);
        add2 --> mul3(*);
        mul2 --> 2_2(2);
        mul2 --> x2(x);
        mul3 --> 2_3(2);
        mul3 --> y2(y);

        end

        2_xy -.-> 2x_2y

(note: distribution hasn’t been implemented in my code yet)

Levels/Branches

The one downside to the tree format is that there are always different levels. For example, when adding 1 + a + 3, it doesn’t matter which 2 numbers you add first, but with the tree you have to have an order. So, the tree will look like this:

  
    graph TD;
        subgraph 1a3["(1 + a) + 3"];

        add(+) --> 3(3);
        add --> add2(+);
        add2 --> 1(1);
        add2 --> a(a);  
        
        end

The ideal solution we want is a + 4, but in this format, it will first add 1 + a, which isn’t possible since a is a variable, and then add 3 to 1 + a. This will just result in 1 + a + 3 again.

What we want instead is something like this:

  
    graph TD;
        subgraph 1a3["1 + a + 3"];

        add(+) --> 1(1);
        add --> a(a);
        add --> 3(3);

        end

Now, all of them are on the same “level”. Since addition is commutative, we can just re-arrange the expression and add 1 and 3 like so:

  
    graph LR;
        subgraph 1a3["1 + a + 3"];

        add(+) --> 1(1);
        add --> a(a);
        add --> 3(3);

        end

        subgraph a13["a + 1 + 3"];

        add2(+) --> a2(a);
        add2 --> 1_2(1);
        add2 --> 3_2(3);

        end

        subgraph a4["a + 4"];
    
        add3(+) --> a3(a);
        add3 --> b4(4);

        end

        1a3 -.-> a13 -.-> a4;

And we’ve got a + 4. Perfect!

So, the tree and the textual format are both good for different scenarios, but the tree is helpful when visualizing an expression, and the textual form is good for daily usage.

Go To:

< Intro Four Modules >