Proposal: Global variables in functional programming

jolon
7 min readSep 16, 2022

This might seem like an unusual proposal. Not only is any kind of mutable state not permitted in FP, the concept of global variables is discouraged even in imperative programming. However, I will argue that if you are currently already using FP techniques you already are using a global variables pattern, let me explain…

Global variables in functional programming

The concept of FP is simple, a function can not mutate any state, it simply takes in input parameters and returns a new result, nothing is mutated, certainly not global state.

However, let’s look at this in practice. Most applications and systems run for long periods of time, i.e. they don’t just execute a single task then exit. As a result programs do have state, which is global state.

In FP, there is a new global state on each new iteration. The global state isn’t mutated, it is instead replaced in its entirety even if only part of it is modified.

Even though the global state is not mutated, in practice we could reason that it is, just in a very confined manner. The new global state is conceptually a mutation of the previous global state.

An example

Let’s say we have a mobile app which manages a small local database of notes.

The main event ‘loop’ that we might see in imperative programming is replaced in FP with a recursive function call, effectively:

main(state): 
state = handle_event(state)
state = render(state)
main(state) // Recursive call to main for the next iteration

Note already that state is in fact a global state.

Also note that state is being passed to all functions in main.

The problem

In FP we like to idealise that functions only operate on the input variables that it requires. This is certainly true for utility functions or functions that might be provided by a library, such as math functions, or input/output:

sin(a) -> Float
print(String)

But for long running applications this is not true.

Consider the following global state for our notes app:

global_state:
- settings
- font_size
- dark_mode
- notes[]
- current_screen

Now consider this function call hierarchy in our application:

main(state)
- render(state)
- render_note(state, note)
- render_background(state)
- render_text(state, note)

Notice how every function takes the global state?

Now you could ask why do they all need the global state?

The main reason is because the subsequent render functions need the settings to determine the font_size and dark_mode settings. You could argue that just settings should be passed in not the entire state:

render_note(settings, note)

And even though this is true, anyone with real world experience with FP knows that the global state variable is going to grow very large, and the number of properties of the global state that functions are going to need to access will also grow.

What if the app is cloud connected and each screen needs to also know if the user is logged in and also their user name? What if the app has in-app purchases and functions need to know if they have been purchased? What if the app stores other things rather than just notes that the user can access from within a note? How about calculating view dimensions based on the dimensions and characteristic of other views?

The reality is that the number of things you pass to functions becomes unwieldily long and difficult to maintain, constantly adding and removing parameters, and then propagating that up and down the entire call hierarchy. It starts to discourage creating new functions and functions become large. And even if smaller functions are created they have ridiculously long numbers of arguments.

The reality is we largely just pass the global state to everything!

The global state pattern

Now that we have established that we do effectively have a global state in FP, let’s look at some of the problems with the current immutable restrictions of FP languages.

Here is a typical use case of global state in FP:

main(state, event):
state = handle_event(state, event)
handle_event(state, event):
case event:
BtnClick:
newCount = state.count + 1
[state | count: newCount]

Note that every single function in your app will look like this. Every single function will:

  1. Take state as its first parameter
  2. Will prepend every access to a global property with state (e.g. state.count)
  3. Will need to create a new state value by updating the previous state (e.g. [state | count: newCount]
  4. The state variable must be returned from every function (either implicitly or explicitly)

Every single function!

How can we improve this?

Remember the Ruby on Rails DRY adage? Don’t repeat yourself.

I will provide solutions to the four numbered points above:

1. Global state is passed to every function

A solution to this is to implicitly pass global state to every function. Every function by default has the global state passed to it.

As a result there needs to be a universally accepted way to access the global state since it isn’t provided as a parameter name in each function.

It could be anything, state seems reasonable, or even global is a possibility.

The above example becomes:

main(event):
state = handle_event(event)
handle_event(event):
case event:
BtnClick:
newCount = state.count + 1
[state | count: newCount]

We have simply omitted the state parameter and assume it is available globally (or more accurately assume it is implicitly the first parameter).

2. The state variable is mentioned every time a global state property is accessed

A way around this is to have a shortcut notation to refer to the global state. One possibility is simply a leading period ..

main(event):
state = handle_event(event)
handle_event(event):
case event:
BtnClick:
newCount = .count + 1
[state | count: newCount]

Now we only have two references to the state variable.

3. The global state must be updated

In the previous example I have:

[state | count: newCount]

Which in fact might be expanded to:

state = [state | count: newCount]

We are just assuming that the handle_event function returns the last value (which will be state).

Why can’t we just do this:

state.count = newCount

Or with the abbreviated syntax:

.count = newCount

Or even better:

.count++

Now you might be saying, that’s mutation!

Actually it isn’t, it’s syntactic sugar that simply expands to the original line of code above.

4. The state variable must be returned from every function

The only way we can create a new global state is if we return it from a function.

If every function accepts a ‘hidden’ global state parameter, and can potentially modify it then it must also return it.

Here is a function with hidden global state:

add_one:
.count++

It must also implicitly return the state. If our FP has types and the type of our state is State then the add_one function would be:

add_one -> State:
.count++

If add_one returns a state then the caller must also receive it:

main:
state = add_one()

This is of course duplication, it would be better that it is implicitly assigned to a new state variable:

main:
add_one()

So calls to every function implicitly include a state variable as the first parameter and implicitly assign the result of every function call to the state variable.

What if a function needs to return a value of its own in addition to the global state?

Take the following example (without any of the above state abbreviations):

render(state):
height = calculate_height(state)
calculate_height(state):
state.header.height + state.footer.height

calculate_height returns a height. In our proposed abbreviated form it would also implicitly return the state as well. How do we return both values?

One solution is that all functions return a tuple of the global state and its return value, e.g. (state, return_value). In expanded form:

render(state):
(state, height) = calculate_height(state)
calculate_height(state):
(state, state.header.height + state.footer.height)

And in abbreviated form:

render:
height = calculate_height()
calculate_height():
.header.height + .footer.height)

Extending to any record access

Some of the above abbreviations could also be applied to any record access, not just the global state.

Consider a size variable having its dimensions updated:

size = [size | width: size.width + 10]

Note that some FP languages permit the same variable to be redefined, the variable now points to the new value, the old value has not been mutated.

There is no reason why having property access on the left hand side couldn’t just be syntactic sugar for the above:

size.width = size.width + 10

Or even

size.width += 10

Integration with Classes

Many FP languages don’t have classes, however this abbreviated syntax could be utilised in class functions. It could be assumed that all class instance functions take an instance of the class as its first parameter and always returns a new instance of the class.

For example, this would be the expanded syntax:

class Note:  - text  getWordCount(note):    Str.countWords(note.text)(note, count) = getWordCount(note)

The abbreviated syntax would assume that the instance has been passed as the first parameter and will return a tuple of the new instance and the return value of the function:

class Note:  - text  getWordCount:    Str.countWords(self.text)count = note.getWordCount() // note is also reassigned implicitly

Discussion

No doubt this is a controversial proposal. It looks exactly like global variables. And it also ‘looks’ like mutation. Perhaps it will promote bad coding practices. However, it does not violate any of the immutability constraints of FP and also follows the current practice with the only exception that it is less code and is easier to read.

One argument is that it could/would be dangerous for some functions to have access to the global state and this would certainly be true for library functions. It could be that some functions are specifically marked as not having access to the global state (or vice versa).

Perhaps functions that have implicit access to global state have a @state or @global decorator.

--

--