Pure and impure functional programming
Pure functions are inherently referentially transparent, which allows memoization (caching the result). Lack of mutable state permits reentrancy, allows different versions of linked data structures to share memory, and makes automatic parallelization possible. The point is that by restricting yourself from mutating state, you no longer have to think about many complicated issues of imperative programming.
However, this restriction has drawbacks. One is performance: some algorithms and data structures (like building a hash table) simply can't be expressed as pure functions without having to copy around large amounts of data. Another: Compare with Haskell, a pure functional language. Since mutation does not exist (conceptually), you have to represent state changes using monads. (Though Haskell provides a reasonably concise do
-notation syntactic sugar, programming within a state monad is quite a different language from "regular" Haskell!) If your algorithm is most easily expressed using several interlocking loops that change state, a Haskell implementation will be clunkier than what's possible in an impure language.
An example is changing a single node deeply nested within an XML document. It's possible but more difficult without state mutation, using zipper data structures. Example pseudocode (pure):
old_xml = <a><b><c><d><e><f><g><h attrib="oldvalue"/></g></f></e></d></c></b></a>
// '\' is the XML selection operator
node_to_change = orig_xml \ "a" \ "b" \ "c" \ "d" \ "e" \ "f" \ "g" \ "h"
node_changed = node_to_change.copy("attrib" -> "newvalue")
new_xml = node_changed.unselect().unselect().unselect().unselect()
.unselect().unselect().unselect().unselect()
return new_xml
Example (impure):
xml = <a><b><c><d><e><f><g><h attrib="oldvalue"/></g></f></e></d></c></b></a>
node_to_change = orig_xml.select_by_xpath("/a/b/c/d/e/f/g/h")
node_to_change.set("attrib" -> "newvalue")
return xml // xml has already been updated
For more information on purely functional data structures, see https://cstheory.stackexchange.com/questions/1539/whats-new-in-purely-functional-data-structures-since-okasaki.
(Further, it's often possible to write a procedural function that only manipulates internal state, so that it can be wrapped up so that it is effectively a pure function to its callers. This is a bit easier in an impure language because you don't have to write it in a state monad and pass it to runST
.)
Though writing in an impure style you lose these benefits, some of the other conveniences of functional programming (like higher-order functions) still apply.
Using mutation
Lisp is an impure functional language, meaning that it permits state mutation. This is by design, so that if you need mutation you can use it without practically having to use a different language.
Generally, yes, it's fine to use state mutation when
- it's needed for performance reasons, or
- your algorithm can be expressed more clearly using mutation.
When you do so:
- Clearly document that your
uniquify
function will mutate the list you pass to it. It would be nasty for the caller to pass your function a variable and have it come back changed!
- If your application is multithreaded, analyze, be aware of, and document whether your impure function is thread-safe.