HiveBrain v1.2.0
Get Started
← Back to all entries
patternMajor

Why do functional languages disallow reassignment of local variables?

Submitted by: @import:stackexchange-cs··
0
Viewed 0 times
localwhylanguagesdisallowreassignmentvariablesfunctional

Problem

Fair warning: I don't actually know a functional language so I'm doing all the pseudocode in Python

I'm trying to understand why functional languages disallow variable reassignment, e.g. x = x + 1. Referential transparency, pure functions, and the dangers of side effects are all mentioned, but the examples tend to go for the low-hanging fruit of functions that depend on mutable globals, which are also discouraged in imperative languages.

My question involves variables created and mutated within the function. For example:

def numsum1(n):
    sum = 0
    i = 1
    while i <= n:
        sum = sum + i
        i = i + 1
    return sum


The functional way of doing this seems to be tail recursion, where the updated sum and i are passed from function call to function call. I know that there are existing higher-order functions for this, but I think this illustrates the similarity to numsum1 more plainly:

def numsum2(n): return numsumstep(0, 1, n)

def numsumstep(sum, i, n):
    if i <= n:
        return numsumstep(sum + i, i + 1, n)
    else:
        return sum


numsum1 and numsum2 do the exact same thing (with tail call optimization) and are both referentially transparent. I do see why numsum1 is internally referentially opaque; the expressions i + 1 and sum + i change in value with each iteration and thus cannot be replaced by a constant value. But why does that matter if numsum1 itself is referentially transparent? Are there examples of functions that become referentially opaque solely because of reassigning local variables?

Solution

In a pure functional programming language, there is no real notion of time at all. So, saying that a variable x has value a at one point and then b later simply doesn't make any sense – it's like asking a character in a painting why she always stares in the same direction.

The advantage of having no time is that you never† need to worry about the order in which computations happen. If a variable is in scope then it also has the correct value, i.e. the value it has been assigned. (Which assignment may actually be “after” the computation in which it is needed – definitions can be reordered at will.)

Whereas in an imperative language – well, consider this program:

def numsum1(n):
    sum = 0
    i = 1
    while i = 0:
        sum = sum - i
        i = i - 1
    return (midterm, i)


If for some reason you need to refactor and pull the midterm definition behind the second loop, overlooking that it actually mutates sum again, then you would get the wrong result.

Now, you might well argue that this is defeated if you need to use recursion to basically fake mutation. Isn't there just as much, or even more, potential for mistakes if you have a recursive call using a parameter still called x that is effectively the same variable anyway?

– Not quite, because outside of the recursive calls the variable is guaranteed to stay the same. The refactoring problem with the above example wouldn't happen in a functional language.

Furthermore, as Odalrick already wrote, recursion isn't actually what's normally used to replace loops in functional languages. The idiomatic Haskell version of your program is

import Data.List (sum)

numsum :: Int -> Int
numsum n = sum [1..n]


...or, using more general-purpose tools,

numsum n = foldl' (+) 0 . take n $ iterate (+1) 1


†That's a bit of an exaggeration. Of course, you do sometimes need to take time into account even in a functional language. Obviously, if it runs somehow interactively (IO monad in Haskell), then those parts are subject to latency considerations. And even for completely pure computations, one side effect that you can't possibly avoid is memory consumption. And that's indeed the one thing that Haskell truely isn't good at: it's really easy to write code that typechecks, works, is correct, but takes gigabytes of memory (when a few kilobytes should have been enough) because some thunks are never garbage-collected.

Code Snippets

def numsum1(n):
    sum = 0
    i = 1
    while i <= n:
        sum = sum + i
        i = i + 1
    midterm = sum
    while i >= 0:
        sum = sum - i
        i = i - 1
    return (midterm, i)
import Data.List (sum)

numsum :: Int -> Int
numsum n = sum [1..n]
numsum n = foldl' (+) 0 . take n $ iterate (+1) 1

Context

StackExchange Computer Science Q#143180, answer score: 32

Revisions (0)

No revisions yet.