Thinking about State
When I started programming my teachers would always stress the importance of keeping programs simple and reducing complexity. Now, when I advise others getting started, I also stress the same things, but instead I frame it in terms of thinking about state.
Everyone, beginner or professional, should think of their programs and their possible states. It is fundamental to writing good, maintainable software and an essential part of making trade-offs in your programs. It also reveals the true purpose of abstractions and how useful they are as tools for managing complexity and state.
What is state?
The simplest state in a computer is the bit. Wether a single-bit is on (
1) or off (
0). 8-bits, known as a byte, can either be 8 separate states or they can be combined together to represent 256 states. With one byte the set of possible states can range between simple (16) and complex (256) depending on how it’s used by the code.
Programming languages provide abstractions for numbers of various forms and strings so we do not need to use bytes directly. These abstractions let us focus working with the variables of different types. Each variable makes up a part of the state of your program. All the possible values of those variables are the set of possible states which your program could be in. If you’re not careful often that number is larger than you’d need it to be.
Complex state and complex programs
As before with the byte, how a variable is used affects it’s possible set of states and how complicated it is to understand. How the program is written helps narrow the possible states. A program which has many possible states is one which is complex.
Complex programs are hard to understand because there are so many possibilities of the state and how the program will react to that state. Two ways to making your programs less complex is reducing the complexity and using stricter types.
The first way to making a program less complex is simply by reducing the possible states your application could be in.
Let’s make up an example that shows either a radio or check box on the screen. We will make a function called
makeElement which will take a string of either
The complexity here is that a string can have anything in it from
” “ to
”radcek” and neither of those are ones we expected. One way to avoid this problem is simply using a boolean so we only have
false to check.
Reducing complexity is important, but there are many times where you can’t simply reduce the complexity. In the example the changes we made make the code harder to read because
makeElement(false) doesn’t clearly mean “show a checkbox”. I also think back to some websites and robotics projects I’ve worked on where there simply wasn’t a way to reduce the state because the problem being solved was just a complex problem.
Computers are powerful and people want to use that power to handle complexity for them. So there is a point where you must simply handle the complexity in a way that can be maintained and understood.
Programming languages with stricter type systems let you place limits how variables are used.
If we were writing the example above in TypeScript we could have defined a type which narrows the possible strings to just “radio” and “check” like this
type ElementType = “radio” | “check”;
Now instead we’ve told TypeScript that when it tries to compile this code anyone calling
makeElement(...) must pass only one of those two values and
makeElement doesn’t need to worry about, or handle, all the other possibilities because we’ve narrowed the state to just those two possibilites.
Many languages can’t narrow state because their type system doesn’t support doing this, but instead you can check at the top of a new scope that everything is what you expected. Python does not have any strict typing on it’s own. It’s common to
assert that variables only have acceptable values and write automated tests to make sure that as the function changes it doesn’t suddenly allow values it wasn’t supposed to.
Languages with stricter type systems allow you to write fewer tests because they narrow the possible states and the need to test for those possible states being handled. Your program is less complex so it requires less rigorous testing.
Often new programs are scolded for using Global Variables as they’re getting started, but it’s never really explained why they’re being scolded. The fixes recommended are often to simply move around those variables which somehow makes it better. It’s never explained so programmers get this idea that simply putting variables and functions in specific places with specific names is good, but don’t understand what makes it good. Hence why I like to stress the importance of thinking of state.
As we’ve covered, complexity in a program comes from the possible states the program could be in. Global variables (‘aka “Global State”) are just variables that any part of your program can access or update. How your code works with variables determines the possible set of states so if any code could change that variable it means every usage of that variable must be checked and understood.
The point of not using Global State is simply to make your program less complex by reducing the possible ways your variables can be used or changed. Doing work in a function or a class is just different ways of narrowing what can change or access those local variables and makes it easier to write the code correctly. Outside of that function or class doesn’t need to worry about those variables either.
There are times when using global state makes sense for performance reasons, but doing that will make your program more complex. It shouldn’t be done unless:
- You know and verify it’ll make the program faster.
- You actually need it to go faster
Sometimes changes are complex and do not actually make the progam faster in a meaningful way. Sometimes it does make it way faster, but it’s not something anyone cares about, but now that area of the code is more complex and harder to maintain in the future.
Code is read far more than written and making things easier to maintain in the future is far more important than making them the fastest they could be. Hence you should make that trade off only when you have a reason to.
I didn’t learn to think about my programs in terms of state until long after I’d learned to program. Once exposed to the idea, it took a long time for me to get it. The beginner misconceptions I mentioned were all ones I had. Specifically the one about moving variables and functions around without any specific purpose.
Thinking about state helped me reduce the complexity of programs I write, lessen the number of bugs, and better explain to others on my team on how and why to improve their code.
I hope this post helped you better understand state and why you should think about the possible states of your program so you can get the same benefits I have.
Any comments or suggestions? Mention me on Twitter: @0xaio