Is having mutable local variables in a function that are only used internally, (e.g. the function has no side effects, at least not intentionally) still considered “non functional”?
e.g. in the “Functional programming with Scala” course style check considers any var
usage as bad
My question, if the function has no side effects, is writing imperative style code still discouraged?
e.g. instead of using tail recursion with the accumulator pattern, what’s wrong with doing a local for loop and creating a local mutable ListBuffer
and adding to it, as long as the input is not changed?
If the answer is “yes, they are always discouraged, even if there is no side effects” then what is the reason?
3
The one thing that is unequivocally bad practice here is claiming that something is a pure function when it isn’t.
If mutable variables are used in a way that is truly and completely self-contained, the function is externally pure and everyone is happy. Haskell in fact supports this explicitly, with the type system even ensuring that mutable references can’t be used outside the function that creates them.
That said, I think talking about “side effects” is not the best way to look at it (and is why I said “pure” above). Anything that creates a dependency between the function and external state makes things harder to reason about, and that includes things like knowing the current time or using concealed mutable state in a non-thread-safe way.
The problem isn’t mutability per se, it’s a lack of referential transparency.
A referentially transparent thing and a reference to it always have to be equal, so a referentially transparent function will always return the same results for a given set of inputs and a referentially transparent “variable” is really a value rather than a variable, since it can’t change. You can make a referentially transparent function that has a mutable variable inside; that isn’t a problem. It might be harder to guarantee the function is referentially transparent, though, depending on what you’re doing.
There’s one instance I can think of where mutability has to be used to do something that is very functional: memoization. Memoization is caching values from a function, so they don’t have to be recomputed; it’s referentially transparent, but it does use mutation.
But in general referential transparency and immutability go together, other than a local mutable variable in a referentially transparent function and memoization, I’m not sure there are any other examples where this is not the case.
5
It’s not really good to boil this down to “good practice” vs “bad practice”. Scala supports mutable values because they solve certain problems much better than immutable values, namely those that are iterative in nature.
For perspective, I’m fairly sure that via CanBuildFrom
almost all immutable structures provided by scala do some sort of mutation internally. The point is that what they expose is immutable. Keeping as many values immutable as possible helps make the program easier to reason about and less error prone.
This doesn’t mean that you necessarily need to avoid mutable structures and values internally when you have a problem that is better suited to mutability.
With that in mind, a lot of problems that typically require mutable variables (such as looping) can be solved better with a lot of the higher order functions that languages like Scala provide (map/filter/fold). Be aware of those.
1
Aside from potential issues with thread safety, you also typically lose a lot of type safety. Imperative loops have a return type of Unit
and can take pretty much any expression for inputs. Higher-order functions and even recursion have much more precise semantics and types.
You also have many more options for functional container processing than with imperative loops. With imperative, you basically have for
, while
, and minor variations on those two like do...while
and foreach
.
In functional, you have aggregate, count, filter, find, flatMap, fold, groupBy, lastIndexWhere, map, maxBy, minBy, partition, scan, sortBy, sortWith, span, and takeWhile, just to name a few more common ones from Scala’s standard library. When you become accustomed to having those available, imperative for
loops seem too basic in comparison.
The only real reason to use local mutability is very occasionally for performance.
0
I would say it is mostly ok. What’s more, generating structures this way might be a good way to improve performances in some cases. Clojure has takled this problem by providing Transient data structures.
The basic idea is to allow local mutations in a limited scope, and then to freeze the structure before returning it. This way, your user can still reason about your code as if it were pure, but you are able to perform in place transformations when you need to.
As the link says:
If a tree falls in the woods, does it make a sound? If a pure function
mutates some local data in order to produce an immutable return value,
is that ok?
Having no local mutable variables does have one advantage–it makes the function more friendly towards threads.
I got burned by such a local variable (not in my code, nor did I have the source) causing a low-probability data corruption. Thread safety wasn’t mentioned one way or another, there was no state that persisted across calls and there were no side effects. It didn’t occur to me that it might not be thread safe, chasing a 1 in 100,000 random data corruption is a royal pain.