5.2 State
The random_number function shows a different behavior from the functions we have seen so far. Until now, all the functions we have defined behaved according to the rules of mathematics: given a set of arguments, the function produces results and, more importantly, given the same arguments, the function always produces the same results. For example, regardless of the number of times the factorial function is called, for a given argument, it will always produce the factorial of that argument.
The random_number function is different from the others because, besides not needing any argument, it produces a different result each time it is called.
From a mathematical point of view, a function without parameters is not uncommon: it is precisely what is known as a constant. In fact, just as we write \(\sin \cos x\) to designate \(\sin(\cos(x))\), we can just as well write \(\sin \pi\) to designate \(\sin(\pi())\), where \(\pi\) can be seen as a function without arguments.
From a mathematical point of view, a function that produces different results each time it is called is anything but normal. In fact, according to the mathematical definition of function, this behavior is not possible. And yet, this is precisely the behavior we would like the random_number function to have.
To obtain such behavior, it is necessary to introduce the concept of state. We say that a function has state when its behavior depends on its history, i.e., on its previous calls. The random_number function is an example of a function with state, but there are many other examples in the real world. For example, a bank account has a state that depends on all past transactions. A car’s fuel tank also has a state that depends on the past tank fillings and journeys.
For a function to have history it must have memory, i.e., it must recall past events in order to influence future results. So far, we have seen that the = operator allows us to define associations between names and values. However, what has not yet been discussed is the possibility to modify these associations, i.e., change the value that was associated to a particular name. It is this feature that allows us to include memory in our functions.
In the case of the random_number function, the memory that is important to us is the last random number generated. Thus, suppose we had an association between that number and the name previous_random_number. Initially, this name should be associated with the seed of the random number sequence:
previous_random_number = 12345
Now, we can define a random_number function that, not only uses the last value associated with the name previous_random_number, but also updates this association with the new generated value:
random_number() =
previous_random_number = next_random_number(previous_random_number)
Unfortunately, when we try to use the function, it does not seem to work as intended:
> random_number()
ERROR: UndefVarError: previous_random_number not defined
The result is a consequence of the Julia’s rule that says that whenever a name is assigned to a value inside a function, that name is treated as a local variable, meaning that it is only visible inside the function and, moreover, it is only active while the function is being called. Furthermore, a variable that is local to a function makes a global variable with the same name invisible. This is called shadowing, and we say that the local name shadows the global one, making it inaccessible inside the function.
This rule makes sense in many situations, but not in this one, since it implies that the assignment made does not affect the global name as was intended but, instead, the local one. To make things worse, in this particular case, the assignment depends on the previous value assigned to that same name but, given that this name is treated as a local one, it does not have a value yet, so, it is undefined and cannot be used. That is precisely what the error message is saying.
In order to fix this problem, Julia provides a declaration that allows us to assert that a given name refers to a global definition. We will use that declaration to redefine the random_number function:
random_number() =
global previous_random_number = next_random_number(previous_random_number)
Using this definition, the function has the intended behavior.
> random_number()
2822
> random_number()
11031
As we can see, every time the random_number function is called, the value associated with the previous_random_number is updated, influencing the function’s future behavior. Obviously, at any given moment, we can restart the sequence of random numbers simply by restoring the seed value:
> random_number()
21180
> previous_random_number = 12345
12345
> random_number()
2822
> random_number()
11031
> random_number()
21180