The Hyperpessimist

The grandest failure.

Avoiding Action at a Distance Is the Fast Track to Functional Programming

That’s an odd title, for sure. What is “action at a distance”? Well, how about an example? Let’s take Python 3.x, but it would work very much the same with JavaScript. Or Lua. Or most other languages.

1
2
3
4
lat = [1, 2, 3]
for e in [4, 5, 6]:
    lat.append(e)
print(lat)

A perfectly fine program, right? It adds some elements to a list, nothing unusual (yes, I know extend exists, but extend is only a shortcut for exactly this functionality), you might see that in every Python program ever.

Now, how about we wrap that in a function?

1
2
3
4
5
6
lat = [1, 2, 3]
def extend():
    for e in [4, 5, 6]:
        lat.append(e)
extend()
print(lat)

That’s something most semi-knowledgeable Pythoneers would object, since you can call that function anywhere and the value of lat magically changes. This is what I was calling “action at a distance”. You can call that also “nonlocal modification/assignment”, and Python 3 even has a nonlocal keyword to allow such nonlocal assignments.

So, looking at the function, it takes no arguments and returns None. Python-programmers will say: ok, then, make lat an argument and return the new list instead.

1
2
3
4
5
6
7
lat = [1, 2, 3]
def extend(lst):
    for e in [4, 5, 6]:
        lst.append(e)
    return lst
lat = extend(lat)
print(lat)

Great, now the function is not able to magically change lat any time it wants. When we look into this code, we have basically run into a circle: the for loop “takes” no input data and “returns” no output data, it just magically changes lst. This is a small toy function, so it is easy to see, but conceptually we can think about it again as a function that does action on a distance.

The usual way is to do so is to write functions that take arguments and return some new values. Unfortunately, append does not return a value, it, again, does action at a distance in some way, “magically” adding an element to the list. That’s easy to fix, let’s make our own append operator:

1
2
def append(lst, e):
    return lst + [e]

With this in order, we would need to call append(lst, 4) and then append(that_result, 5) and append(previous_result, 6) on it, thus constructing append(append(append(lst, 4), 5, 6). Fortunately, Python ships with a function that does exactly that, in 2.x it is called reduce and in 3.x it is still called reduce but moved into the functools module.

1
2
def extend(lst):
    return functools.reduce(append, [4, 5, 6], lst)

The whole code looks like this now:

1
2
3
4
5
6
7
8
import functools
lat = [1, 2, 3]
def append(lst, e):
    return lst + [e]
def extend(lst):
    return functools.reduce(append, [4, 5, 6], lst)
lat = extend(lat)
print(lat)

This time we eliminated all action-at-a-distance operations. Every operation takes an input and returns a result, there is no more things changing in the background without an assignment. Interestingly enough, we have just invented functional programming, a paradigm that avoids action-at-a-distance as much as possible, which makes reasoning about code easier. This is, by the way called “referential transparency”, a fancy-pants name for “nothing happens at a distance”.

Would you think, that there are people who made programming languages which only do functional programming?