State and Behavior

The data stored in a program at any given moment (in the form of variables, objects, etc.) is the state of the program. Consider a variable:

int a = 5;

The state of the variable a after this line is 5. If we then run:

a = a * 3;

The state is now 15. Consider the Vector3 struct we defined in the last section:

public struct Vector3 {
    public double x;
    public double y;
    public double z;

    // constructor
    public Vector3(double x, double y, double z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

If we create an instance of that struct in the variable b:

Vector3 b = new Vector3(1.2, 3.7, 5.6);

The state of our variable b is $\{1.2, 3.7, 5.6\}$. If we change one of b’s fields:

b.x = 6.0;

The state of our variable b is $\{6.0, 3.7, 5.6\}$.

We can also think about the state of the program, which would be something like: $\{a: 5, b: \{x: 6.0, y: 3.7, z: 5.6\}\}$, or a state vector like: $|5, 6.0, 3.7, 5.6|$. We can therefore think of a program as a state machine. We can in fact, draw our entire program as a state table listing all possible legal states (combinations of variable values) and the transitions between those states. Techniques like this can be used to reason about our programs and even prove them correct!

This way of reasoning about programs is the heart of Automata Theory, a subject you may choose to learn more about if you pursue graduate studies in computer science.

What causes our program to transition between states? If we look at our earlier examples, it is clear that assignment is a strong culprit. Expressions clearly have a role to play, as do control-flow structures decide which transformations take place. In fact, we can say that our program code is what drives state changes - the behavior of the program.

Thus, programs are composed of both state (the values stored in memory at a particular moment in time) and behavior (the instructions to change that state).

Now, can you imagine trying to draw the state table for a large program? Something on the order of EPIC?

On the other hand, with encapsulation we can reason about state and behavior on a much smaller scale. Consider this function working with our Vector3 struct:

/// <summary>
/// Returns the the supplied vector scaled by the provided scalar
/// </summary>
public static Vector3 Scale(Vector3 vec, double scale) {
    double x = vec.x * scale;
    double y = vec.y * scale;
    double z = vec.z * scale;
    return new Vector3(x, y, z);
}

If this method was invoked with a vector ${4.0, 1.0, 3.4}$ and a scalar ${2.0}$ our state table would look something like:

step vec.x vec.y vec.z scale x y z return.x return.y return.z
0 4.0 1.0 3.4 2.0 0.0 0.0 0.0 0.0 0.0 0.0
1 4.0 1.0 3.4 2.0 8.0 0.0 0.0 0.0 0.0 0.0
2 4.0 1.0 3.4 2.0 8.0 2.0 0.0 0.0 0.0 0.0
3 4.0 1.0 3.4 2.0 8.0 2.0 6.8 0.0 0.0 0.0
4 4.0 1.0 3.4 2.0 8.0 2.0 6.8 8.0 2.0 6.8

Because the parameters vec and scale, as well as the variables x, y, z, and the unnamed Vector3 we return are all defined only within the scope of the method, we can reason about them and the associated state changes independently of the rest of the program. Essentially, we have encapsulated a portion of the program state in our Vector3 struct, and encapsulated a portion of the program behavior in the static Vector3 library. This greatly simplifies both writing and debugging programs.

However, we really will only use the Vector3 library in conjunction with Vector3 structures, so it makes a certain amount of sense to define them in the same place. This is where classes and objects come into the picture, which we’ll discuss next.