How to Switch from the Imperative Mindset
βGrokking Simplicity is by far the gentlest introduction to FP I've encountered and I've been recommending it to people that want an on-ramp to FP ever since.β
Summary: Functional programming, from one perspective, is just a collection of habits that affect our programming. I've identified the cues for those habits and a routine for replacing imperative code with functional code.
As programmers, we all have a history. Many of us coming to functional programming have learned imperative programming and want to switch to a functional mindset.
Let me first say that any programming skills are a help, not a hinderance, to learning functional programming. It is much harder to learn to program the first time than to switch mindsets to a functional paradigm.
That said, it will be a challenge to establish new habits and to break old ones. But once you do, functional programming will inform how you code everything you do. It will give you a new perspective with which to design your programs. And it is very common to hear people say that learning functional programming has made their software design better.
I'm going to use the framework presented in The Power of Habit to help you build the new habits to replace your old one. The framework is simple: identify the Cue (or the trigger) for a behavior, then modify the Routine, then give a Reward. I'll let you come up with the reward, but I like to revel in the clarity of my code.
A critical decision
There's an important choice to make before you begin this change. It depends on your temperament and circumstances. Do you prefer a drastic all-or-nothing approach or a more gradual approach?
All or nothing
Do you want to jump right in and be forced to program functionally? Maybe you want to totally get outside your comfort zone and start programming in Haskell. Haskell will force you to think functionally or you won't be programming at all, because Haskell is a purely functional language. Some people appreciate the all-or-nothing approach to prevent backsliding.
Step by step
But if you want a gentler approach, maybe you want to try out functional programming in a language you're already familiar with. Perhaps you want to dip your toes in a little, test the waters, and move in gradually. This is also a valid strategy. You can begin to make a new program using functional techniques but in a language you feel comfortable in. Trust me, you can do functional programming in any language. The difference is in how much the language helps you to do it. The danger with this approach is that you've already got lots of established habits and it will be easiest simply to use those.
You could also find some existing code that you're familiar with and refactor it into a functional style. This is also helpful. It will give you practice and improve a working system. Just make sure the code is working before you try this. Otherwise, you might think the functional refactoring has broken it.
Disclaimer
Before we get into the tools, I want to emphasize that I'm not saying that imperative habits are bad. It's easy to moralize habits, and that's not what I want to do. What I am suggesting is that having two ways to solve a problem is better than having one. And once you program in a functional style, you will find that it often has benefits over the imperative style. Sometimes it won't be better, but sometimes it will be shorter, clearer, and more often correct.
The main tools of functional programming
There are three main tools that are universal among functional languages. These tools are always available to the functional programmer. If I meet someone who does not know these tools, this is where I start. They are:
map
filter
reduce
(sometimes calledfold
)
Chances are, your language has something like these. We need to learn to identify when to use them and begin applying these functions right away.
I'm going to use JavaScript for the examples because it's very popular and I know it. The functional examples will use Underscore.
map
map
takes a function and one collection and returns another collection
with the elements transformed by the function.
The habit:
Cue: Applying a pure function (or method) on each element of a list.
Routine: Use map
instead of a loop. Use the function as an argument
to map, or create a function if it's a method call.
Example: Convert the names to upper case.
Imperative:
var names = ["Johnny", "Christine", "Frank", "Juliette"];
var i;
for (i = 0; i < names.length; i++) {
names[i] = names[i].toUpperCase();
}
We're applying the same method to every element of the list.
Functional:
var names = ["Johnny", "Christine", "Frank", "Juliette"];
var newNames = _.map(names, function (n) {
return n.toUpperCase();
});
Because calling a method on elements of a list is so common in JavaScript, Underscore provides an even more concise (and still functional) way to do this (as long as the method is pure).
Underscore alternative:
var names = ["Johnny", "Christine", "Frank", "Juliette"];
var newNames = _.invoke(names, "toUpperCase");
(ECMAScript 5.1 defines Array.prototype.map, but that's not available in all browsers, so I've used the Underscore version.)
If you want more examples and practice, look at these resources: 1 and 2.
filter
The second tool in the toolbox is filter. When you want to keep only some of the items in a collection, this is what you grab.
Cue: An if
with no else inside a loop over a list.
Routine: Use filter
instead of a loop. Move the test of the
conditional into a predicate function. Use the return value of filter
.
Example: Make a list of the names that start with "J".
Imperative:
var names = ["Johnny", "Christine", "Frank", "Juliette"];
var JNames = [];
var i;
for (i = 0; i < names.length; i++) {
if (names[i].indexOf("J") === 0) {
JNames.push(names[i]);
}
}
We see a for
loop with an if
statement inside. We're building up a
new list with only some of the elements.
Functional:
var names = ["Johnny", "Christine", "Frank", "Juliette"];
var JNames = _.filter(names, function (n) {
return n.indexOf("J") === 0;
});
We replace the for
loop with a call to filter
with the same test we
used before.
reduce
The third tool, and arguably the most difficult to learn but the most
rewarding, is reduce
. reduce
takes the elements of a collection and
applies a function to each in turn, resulting in a single value. You
could reduce a list of numbers with +
, which gives you the sum of all
of them. Or you could reduce it with *
, which gives you the product.
Cue: Operating on an initialized variable for each element in a list.
Routine: Use reduce
instead of a loop. Identify the initial state
and identify the function. Use them as arguments to reduce
.
Example: Count all the characters in the names.
Imperative:
var names = ["Johnny", "Christine", "Frank", "Juliette"];
var charCount = 0;
var i;
for (i = 0; i < names.length; i++) {
charCount = charCount + names[i].length;
}
We see an initialized variable, a for
loop, and an operation (+
) on
the variable for each element.
Functional:
var names = ["Johnny", "Christine", "Frank", "Juliette"];
var charCount = _.reduce(
names,
function (c, n) {
return c + n.length;
},
0
);
The operation is adding the length and the initial value is 0.
reduce
can be a doozy so I suggest these resources:
1, 2,
3.
Using all three together
Then, when you've learned to use them each alone, you will begin to see how they may be used together.
Example: Count the characters in names starting with "J".
Imperative:
var names = ["Johnny", "Christine", "Frank", "Juliette"];
var charCount = 0;
var i;
for (i = 0; i < names.length; i++) {
if (names[i].indexOf("J") === 0) {
charCount = charCount + names[i].length;
}
}
We can see all three cues: an if
statement without an else, an
operation on the elements, and an operation on an initialized variable.
Functional:
var names = ["Johnny", "Christine", "Frank", "Juliette"];
var charCount = _.chain(names)
.filter(function (n) {
return n.indexOf("J") === 0;
})
.map(function (n) {
return n.length;
})
.reduce(function (a, b) {
return a + b;
}, 0)
.value();
Here I've also used the Underscore
chain
function to chain operations
together.
There are many more functional operations, but these three are the core. And they all share a very import feature: they return a brand new array instead of modifying the arguments. They help you minimize mutation.
Minimize mutation
Mutation means changing a value in place. It's a type of side-effect that deserves its own mention. Unlike other kinds of side-effects, mutation doesn't have an effect on the outside world (like printing to the screen, sending an email, or launching a missile). It happens only inside the computer's memory. As such, it doesn't serve much purpose besides introducing an implicit order to an execution. An assignment statement implicitly divides the execution of a program into before the assignment and after the assignment. In a complex program with many assignments, it becomes difficult to follow that timeline. It's best to minimize the number of mutations you do.
The habit:
Cue: Modifying a variable, list, or object in place.
Routine: Replace the mutation with operations on fresh copies, as you
would get by using map
, filter
, reduce
, and other pure functions.
Minimize implicit dependencies
Sometimes you have some code that uses the value of a variable outside of its scope. That variable is changing, so the code executes differently every time. In functional programming, we seek to minimize code whose execution depends on things outside of its scope.
The habit:
Cue: A function references the value of a variable outside of its scope.
Routine: Make the variable an argument to the function to make the dependency explicit.
Isolate side-effects
The last habit to establish is to isolate side-effects. Side-effects are code that have an observable effect on the world. Launching a missile or printing to the screen are side-effects. Functional programming separates out side-effecting code from code without side-effects.
We need side-effects to do interesting stuff. But we want to separate the side-effects out so that they aren't spread around our code. We want to be as confident as possible of what code can be run without fear of launching a missile.
So here's our last habit:
Cue: We see some side-effect among our pure code.
Routine: Isolate the side-effecting code from the pure code. Make the non-side-effecting parts into a unit and the side-effecting parts into a unit, then combine them together.
Example: Print only names with five letters.
Imperative:
var names = ["Johnny", "Christine", "Frank", "Juliette"];
var i;
for (i = 0; i < names.length; i++) {
if (names[i].length === 5) {
console.log(names[i]);
}
}
More Functional:
var names = ["Johnny", "Christine", "Frank", "Juliette"];
// this function has a side-effect
function printName(name) {
console.log(name);
}
function filterLength(list, len) {
return _.filter(list, function (s) {
return s.length === len;
});
}
// side-effecting part
_.each(filterLength(names, 5), printName);
Though this is a simplistic example, we can imagine a function like
filterLength
is useful elsewhere. Since it doesn't have any
side-effects, we can use it without worrying about printing.
Conclusions
Functional programming, like other paradigms, can be seen as a bunch of habits that we apply during programming. I've identified several habits, a long with some very common functional tools, that you can begin training in regardless of the language you use. You might find that learning these habits makes your code easier to read and easier to modify.
You may also find that your language doesn't help much to minimize mutation. Or that using these functional idioms is very verbose. In those cases, you may want to try out a more functional language like Haskell or Clojure. If you're into Clojure, I've got a program called PurelyFunctional.tv Online Mentoring. It helps programmers become Clojure professionals. There's also my video course LispCast Introduction to Clojure (which is included in Online Mentoring) that brings you from zero to Clojure, from the syntax through data-driven programming, with animations and exercises.