Learning Clojure/Functional Programming
Functional programming is one of those unfortunate terms in programming shrouded in abuse and confusion but which really shouldn't be all that mysterious. Best understood as an alternative paradigm to imperative programming, functional programming is characterized by:
- functions without side-effects
The term "functional" comes from mathematics, where a function strictly just returns a value based upon zero or more input values and does not produce side-effects nor changes its behavior based upon any values other than those passed to it. Such a "pure" function returns the same thing every time it is called with the same set of values.
This is not the way functions work in imperative (non-functional) programming. In imperative code, functions often:
- produce side-effects (such as writing to a file or some I/O device)
- mutate their inputs and global variables
- change their behavior based upon state outside the function
Pure functions have two big upsides:
- Functions which avoid mutating local data and avoid reading or mutating global data are easier to understand and more amenable to proofs of correctness.
- Functions which avoid reading and writing shared state can be run concurrently without the usual concerns of concurrency.
Now, the obvious objection to writing all our functions to run without side-effects is that we need side-effects for our programs to ultimately do anything useful. As the saying goes, a program without side-effects does nothing more than make your CPU hot. The ideal in functional programming, then, is simply to isolate state changes, not eliminate them entirely. In a pure functional language, like Haskell, functional purity is enforced by the compiler such that all potential side-effects must be explicitly circumscribed in code. An impure functional language, though, like Clojure, does no such enforcement: Clojure helps you structure your code in a functional way, but it is ultimately up to you to keep side-effects out of your purely functional code.
- immutable data
If functions are to avoid mutating state, and if they are to avoid relying upon mutating state, it makes sense then to simply make as much data as possible immutable: if data can't change, there's no danger of it maybe changing. In Clojure, the standard collection types are immutable, and in fact, even local variables are immutable: once defined, the value bound to a local does not change.
You're likely thinking this makes Clojure sound unusable, but it is surprising how much work can be done without mutable data. It's true that real world programs can't have entirely immutable data, but much more data can be immutable than is commonly believed by programmers accustomed only to imperative programming.
- first-class functions
- function-based control flow
In an imperative language, control flow is achieved via special constructs, e.g. if-else statements. In functional languages, control flow mechanisms are either functions or at least function-like in that they return values. For instance, if in Clojure executes one of two branches and returns the value returned by that branch. Such constructs allow for idioms that otherwise would require mutating variables.