If you consider a program where there is no state whatsoever, and the program is not able to read external states (for example, checking the system time), that would entail that any function, besides what values may be passed as parameters, would have no way of differentiating any different circumstances because no different circumstances can exist.
With this class of program, the run-time is effectively redundant as an optimizing compiler could derive any resulting outputs statically. Its essentially equivalent to writing out a large math equation. You can solve it, but the idea of "running the program" is redundant because it has an output inherent to the program itself that cannot change.
Obviously this doesn't resemble most programs, even ones that may be described as "stateless". Usually there is some minimal kind of state, such as input initially passed to the program. For example, imagine a program that outputs the first N digits of the square root of a number K. This program now has an initial state, but beyond knowing what N and K are, the program doesn't need to track program-level states like what day of the week it is, whether or not the user prefers MDY vs DMY date format, etc. However, since the program almost certainly involves some kind of dynamic looping to find N digits, it would need to have some kind of state associated with the loop (for example an iteration number)
So when code is referred to as "stateless" its not a 100% promise, but a kind of qualifier of the degree to which the code depends on state.
So what are the advantages and disadvantages here? The more your code depends on state, the more likely it is to do something that the programmer wasn't expecting. Remember, state is something inherent to the runtime. But we don't write code at runtime. We can try to imagine all the different possible runtime states that could happen, but this quickly gets out of hand.
Examples
Here's an example of the same python program in a stateful vs stateless way.
stateful.py
import math
angle_type = 'radians'
def cosecant(x):
if angle_type == 'radians':
return 1/math.sin(x)
elif angle_type == 'degrees':
return 1/math.sin(x*math.pi/180)
else:
raise NotImplementedError('cosecant is not implemented for angle type', angle_type)
================== RESTART: F:/Documents/Python/stateful.py ==================
>>> cosecant(1)
1.1883951057781212
>>> angle_type = 'degrees'
>>> cosecant(1)
57.298688498550185
>>> angle_type = 'turns'
>>> cosecant(1)
Traceback (most recent call last):
File "<pyshell#16>", line 1, in <module>
cosecant(1)
File "F:/Documents/Python/stateful.py", line 11, in cosecant
raise NotImplementedError('cosecant is not implemented for angle type', angle_type)
NotImplementedError: ('cosecant is not implemented for angle type', 'turns')
>>>
stateless.py
import math
def cosecant(x):
return 1/math.sin(x)
def cosecant_degrees(x):
return 1/math.sin(x*math.pi/180)
================= RESTART: F:/Documents/Python/stateless.py =================
>>> cosecant(1)
1.1883951057781212
>>> cosecant_degrees(1)
57.298688498550185
>>> cosecant_turns(1)
Traceback (most recent call last):
File "<pyshell#19>", line 1, in <module>
cosecant_turns(1)
NameError: name 'cosecant_turns' is not defined
>>>
This example may seem odd, but the same principals and pitfalls extend to more complex programs. We can see how using state here can lead to problems. Consider stateful.py
. One problem is that the person calling cosecant
doesn't necessarily know the current state of angle_type, they would have to set it every time before using cosecant
to make sure that it will do what the want it to do. Since programmers tend to not like repeating themselves, someone may declare mymathlib.angle_type = 'degrees'
at the start of their program, and assume nothing else will ever change it. For example, what if you are working in degrees, but then you call a subroutine that changes angle_type
to 'radians'
and doesn't change to degrees upon finishing.
If other parts of the program are changing it, then the programmer effectively has to set the value to what the want every time before calling it. And even still, if code is running on multiple threads, there is no guarantee that when you set angle_type
to 'degrees'
, another thread didn't immediately set it to something else before your cosecant
call gets executed (a race condition).
In our stateless version of the program, all of these problems disappear. What's the cost? Well now we have 2 different functions instead of 1. Why is this a cost? Well, generally, it is considered good practice to keep an API smaller rather than larger, having multiple tools that do the same thing is confusing for people trying to use your library. Because of this principle, it is sometimes tempting to smoosh 2 or more different functions into 1 if they seem to be doing the same thing. In general this isn't terrible, in fact it often helps a library in terms of maintenance and usability. In this case though, it has caused more harm than good because in the stateful program, while the programmer can effectively use it to do the same thing, we no longer have the simple guarantee that the function will do what you think it will do every time.
This may on the surface seem like a silly example, but consider that python's decimal
module does nearly the same thing. In the decimal
module, states variables including the number of decimal places of precision and rounding rules are stored in the current thread's context
. This isn't quite as problematic as the previous example. Since each thread has its own context
, we don't have to worry about the race condition, but there are still potential trouble spots, like if a subroutine changes state without nicely changing it back for you.
It's easy to rationalize this kind of design and say "a competent programmer should be able to analyze the code and be able to avoid these kind of state related problems if they take the time to think about it". This is true in theory, but if we look at how people actually write code, the principle of least effort trumps everything else most of the time. Look at examples of use of the decimal
module in the official documentation, or any tutorial, and you will notice a common pattern. The references to decimal.getcontext()
outnumber the references to decimal.setcontext()
10 to 1 -- that is if setcontext is mentioned at all, often it is not. In order to manage state in a competent "safety first" kind of way, both of these tools are equally important and if anything decimal.setcontext()
should be used more often since it is what you use to guarantee consistent behavior. Because of this least-effort principle, it is inevitable that programmers, especially beginners, will write code like this that may work in testing, but has no hard guarantee of future safety as the program evolves.
Conclusion
So is stateful code evil, and stateless code is our savior? Well maybe. It could help to think of this little epigram as a guiding principle to avoid certain pitfalls. The reality is that often stateful code is hard to avoid, or even inherent to the program's design. For example, how could we make a video game without state? How do we make a health counter? How does the player move around the level and track its position? Can we have scores or win conditions? Stateful code is not going anywhere, but it does genuinely help to have an understanding of the common pitfalls of designing programs in that way.