Photo by Magda Ehlers from Pexels
What crosses your mind when you hear the phrase "functional programming"? Some people imagine arcane, esoteric code, built around functors and monads and applicatives, but I think of
-
immutability and referential transparency
Immutability just means you create new objects instead of mutating (changing) old ones. This means every variable is effectively
final
orconst
or whatever your language calls it. Instead of
public class Dog { public final String name; public Dog(String name) { this.name = name; } public void rename(String newName) { this.name = newName; } }
yourrename
dDog
would be a new object, à la
public class Dog { public final String name; public Dog(String name) { this.name = name; } public Dog rename(String newName) { return new Dog(newName); } }
Immutability makes it easy to reason about objects (Dog doug = new Dog("doug")
will always havename.equals("doug")
), but it can make your program less efficient, both in terms of memory usage and in terms of performance -- always creating new objects takes more time and space than reusing existing ones.Referential transparency is a closely-related concept. In a nutshell, what it means is that -- wherever you have the value
x
in your code, you can replace it with whatever you initially declaredx
to be. So ifconst x = 10
, thenx
always equals10
. You can replace every instance ofx
with the literal10
and your program should do the exact same thing. (This does not work, of course, if your objects and variables are mutable and you can do something likex = x + 1
!) -
higher-order functions
In many languages, functions and objects are separate things: you can pass objects as arguments to functions, and functions can be scoped as object methods. But languages which encourage a functional style will allow you to pass functions as arguments to functions, like in Scala
def process(text: String, fs: (String => Unit)*): Unit = { fs.foreach(f => f(text)) } process("Hello, World!", println)
The aboveprocess
method takes aString
argumenttext
and a(String => Unit)*
argumentfs
-- that is, zero or more functions which themselves take aString
and returnUnit
, Scala's equivalent ofvoid
. Once you get used to working with higher-order functions, their utility is immense
import Console._ process("this does nothing") process("print to stdout", out.println) process("print to stderr", err.println) process("print to both", out.println, err.println) process("etc", out.println, err.println, myLogFunc)
Adding a new output device is as easy as passing a new function toprocess
. Treating functions as "first-class citizens" of a language opens a huge array of new possibilities. -
pure functions (side-effect-free functions)
A "pure" function is a function without "side effects". What does that mean? In a nutshell, it means that a function should behave similar to a mathematical function (think
y = f(x)
) -- it should take some value,x
, and return or become some other value,y
.Mathematical functions don't write to log files or print text to a terminal or change the value of other variables on the page -- they turn
x
intoy
, that's it.Of course, computer programs are kind of useless if they can't take input from a user, or read a config file, or communicate with external resources. Input / Output, or IO, is required to make programs "interactive". Functions which perform output (typically returning no value) typically return a
void
orUnit
ornull
value of some kind. Haskell, for instance, uses its IO monad for input and output.We can't eliminate side-effecting functions, but we should try to make it as clear as possible what the intent of our functions are. If you want to calculate a value, and log it to a file, separate those concerns into two separate routines, if possible.
-
declarative programming and lambdas
"Functional programming" also makes me think of a particular style of programming: iterating over data structures (like arrays, dictionaries, maps, sets, etc.) using declarative-style functions like
map
,filter
,foreach
, and so on.Imperative programming is (in a sense) the opposite of declarative programming. An imperative program declares exactly what is to be done:
int[10] array = ... double sum = 0; int ii = 0; for (ii = 0; ii < 10; ++ii) { sum += array[ii]; }
A declarative program specifies what is to be done, and the language itself figures out how to do that. For example, in a declarative program, we might rewrite the above as
var sum: Double = 0 array.foreach(x => sum += x) // lambda
...or
val sum = array.fold((partialSum, x) => partialSum += x)
...or even justarray.sum
. Functional programming also tends to make heavy use of "lambda" functions, like the one used above,x => sum += x
. This is an anonymous function (it doesn't have a name likeaddToSum
) which takes a valuex
and adds it to the valuesum
.Declarative programming, and lambda functions, are hallmarks of functional programming, in my view.
-
stream processing
All of the above leads me to routinely return to the (not at all novel) metaphor of plumbing for functional programming. You put data in one end of a "processing pipeline", like water entering a pipe. It may be split into multiple streams, sent down different paths, siphoned off into some database, or dumped ultimately into some data lake, but the data moves from producers / sources (input), through flows / transformers / conductors, and into consumers / sinks.
Stream processing is quite similar to reactive programming, which has a whole manifesto and is implemented in several languages (here's one for Scala, and another for R).
Remember:
"The Internet is... a series of tubes."
-- U.S. Senator Ted Stevens [source]
Wikipedia also mentions lazy evaluation and recursion, but these don't immediately jump into my mind as "pillars" of functional programming.
How about you? What comes to your mind when someone says "functional programming"?