Functional programming often centers around machinery and not core principles. Functional programming is not about monads, monoids, or zippers. It is primarily about writing programs by composing generic reusable functions.
This article is about applying functional thinking when refactoring JavaScript code.
Suppose there are two classes: Employee and Department. Employees have names and salaries, and departments are just simple collections of employees.
function Employee(name, salary) { this.name = name; this.salary = salary; } function Department(employees) { this.works = function(employee){ return _.contains(employees, employee); }; }
Consider the averageSalary
function as a candidate for functional refactoring.
function averageSalary(employees, minSalary, department){ var total = 0; var count = 0; _.each(employees, function(e){ if(minSalary < e.salary && (department == undefined || department.works(e))){ total += e.salary count += 1 }; }); return (count == 0) ? 0 : total / count; }
averageSalary
takes a list of employees, a minimum salary, and optionally a department. Given a department, it calculates the average salary of the employees in that department. When not given a department, it does the same calculation for all the employees.
Use the function as: (Code: AverageSalary – Original)
describe("average salary", function () { var empls = [ new Employee("Jim", 100), new Employee("John", 200), new Employee("Liz", 120), new Employee("Penny", 30) ]; var sales = new Department([empls[0], empls[1]]); it("calculates the average salary", function(){ expect(averageSalary(empls, 50, sales)).toEqual(150) expect(averageSalary(empls, 50)).toEqual(140) } });
Despite the straightforward requirements, the code could appear convoluted, not to mention tricky to extend. Adding another condition requires the signature of the function to change, and the if statement would grow into a real monster.
Now apply functional refactoring to this code.
Use Functions Instead Of Simple Values
Using functions instead of simple values may appear counterintuitive at first, but this results in a powerful technique for generalizing code. In this case it means replacing the minSalary
with a function checking the condition. (Code: averageSalary_1.js)
function salaryCondition(employee) { return employee.salary > 50; } function averageSalary(employees, minSalary, department){ function averageSalary(employees, salaryCondition, departmentCondition){ var total = 0; var count = 0; _.each(employees, function(employee){ if(minSalary < e.salary && (department == undefined || department.works(e))) { if(salaryCondition(employee) && (departmentCondition == undefined || departmentCondition(employee))){ total += employee.salary count += 1 }; }); return (count == 0) ? 0 : total / count; } .... assert.equal(averageSalary(empls, 50, sales), 150); assert.equal(averageSalary(empls, 50), 140) assert.equal(averageSalary(empls, salaryCondition, sales.works), 150); assert.equal(averageSalary(empls, salaryCondition), 140);
Both salary and department conditions become functions. Before implementing both conditions ad-hoc, now the explicit definitions as functions take the same interface. This unification as functions allows passing all the conditions as an array of functions. (Code: averageSalary_2.js)
function averageSalary(employees, salaryCondition, departmentCondition){ function averageSalary(employees, conditions){ var total = 0; var count = 0; _.each(employees, function(e){ if(salaryCondition(employee) && (departmentCondition == undefined || departmentCondition(employee))) { if(_.every(conditions, function(condition){ return condition(employee) })) { total += employee.salary count += 1 }; }); return (count == 0) ? 0 : total / count; } ... assert.equal(averageSalary(empls, salaryCondition, sales.works), 150); assert.equal(averageSalary(empls, salaryCondition), 140); assert.equal(averageSalary(empls, [salaryCondition, sales.works]), 150); assert.equal(averageSalary(empls, [salaryCondition]), 140);
Since an array of conditions is nothing but a composite condition, we can pull out a simple combinator making it explicit. (code: averageSalary_3.js)
While some developers believe in only one way to do something, having combinators available has some advantages when used judiciously. Code that uses lots of combinators tends to have verb names such as powerOf, addOne, compose. Combinators provide a useful service for emphasizing what the code is doing, while other code can emphasize what you’re working with.
function and(predicates){
return function(e){
return _.every(predicates, function(p){return p(e)});
}
}
function averageSalary(employees, conditions){
var total = 0;
var count = 0;
_.each(employees, function(employee){
if(_.every(conditions, function(condition) {
return condition(employee);
})) {
if(and(conditions)(employee)){
total += employee.salary;
count += 1;
};
});
return (count == 0) ? 0 : total / count;
}
The generic and
combinator invites resue as a potential library function.
Intermediate Results
The averageSalary
function has already become more robust. Adding a new condition neither breaks the interface of the function nor changes the implementation.
Model Data Transformations As A Pipeline
Modeling all data transformations as a pipeline provides another useful practice of functional programming. In our case this means extracting the filtering out of the loop. (Code: averageSalary_3.js
function averageSalary(employees, conditions){ var filtered = _.filter(employees, and(conditions)) var total = 0; var count = 0; _.each(employees, function(employee){ if(_.every(conditions, function(condition) { return condition(employee); })) { _.each(filtered, function(employee){ total += employee.salary; count += 1; }); return (count == 0) ? 0 : total / count; }
This change made the counting unnecessary and the code gets deleted.(Code: averageSalary_4.js)
function averageSalary(employees, conditions){ var filtered = _.filter(employees, and(conditions)) var total = 0; var count = 0; _.each(filtered, function(e){ total += e.salary; count += 1; }) return (count == 0) ? 0 : total / count; return (filtered.length == 0) ? 0 : total / filtered.length; }
Next, if we pluck the salaries before adding them up, the summation becomes a simple reduce. Other code gets cleaned up as well. (Code: averageSalary_5.js)
pluck
_.pluck(list, propertyName)
.A convenient version of what is perhaps the most common use-case for
map
: extracting a list of property values.var stooges = [{name: 'moe', age: 40}, {name: 'larry', age: 50}, {name: 'curly', age: 60}]; _.pluck(stooges, 'name'); => ["moe", "larry", "curly"]
function averageSalary(employees, conditions){
var filtered = _.filter(employees, and(conditions));
var count = 0;
_.each(filtered, function(employee){
total += employee.salary;
count += 1;
});
return (count === 0) ? 0 : total / count;
var salaries = _.pluck(filtered, 'salary');
var total = _.reduce(salaries, function(a,b){return a + b}, 0);
return (salaries.length == 0) ? 0 : total / salaries.length;
}
Extract Abstract Functions
Observer that the last two lines have nothing to do with our domain. They contain nothing about employees or departments. They simply implement the average function. So, make the average function explicit. (Code: averageSalary_6.js)
function average(nums){
var total = _.reduce(nums, function(a,b){return a + b}, 0);
return (nums.length === 0) ? 0 : total / nums.length;
}
function averageSalary(employees, conditions){
var filtered = _.filter(employees, and(conditions));
var salaries = _.pluck(filtered, 'salary');
var total = _.reduce(salaries, function(a, b) {return a + b;}, 0);
return (salaries.length === 0) ? 0 : total / salaries.length;
return average(salaries);
}
Once again, an absolutely abstract function emerges.
Finally, after pulling out the plucking of salaries, we get our final solution. (Code: averageSalary_7.js)
function employeeSalaries(employees, conditions){
var filtered = _.filter(employees, and(conditions));
return _.pluck(filtered, 'salary');
}
function averageSalary(employees, conditions){
var filtered = _.filter(employees, and(conditions));
var salaries = _.pluck(filtered, 'salary');
var total = _.reduce(salaries, function(a, b) {return a + b;}, 0);
return (salaries.length === 0) ? 0 : total / salaries.length;
return average(employeeSalaries(employees, conditions))
}
Compare the original and final solutions Does the result illustrate a superior level of abstraction and functionality? First, it is more generic? We can add new types of conditions without breaking the interface of the function. Second, mutable states and if statements do not exist. This makes the code easier to read and understand.
Going Further
Most JavaScript programmers would stop right here and consider the refactoring done, but we can actually go a little bit further.
In particular, we can rewrite averageSalary in point-free style. (Code: averageSalary_7.js)
point-free, also called tacit programming is a programming style in which function definitions do not identify the arguments, or “points” on which the operate. Instead, they merely compose other functions among which are combinators that manipulate the arguments. – Point-free programming
var averageSalary = _.compose(average, employeeSalaries);
We can also spot a generic function hiding in the definition of employeeSalaries.
function pluckWhere(field, list, conditions){ var filtered = _.filter(list, and(conditions)); return _.pluck(filtered, field); }
Which makes the employeeSalaries function trivial.
var employeeSalaries = _.partial(pluckWhere, 'salary');
Dicsussion
If you have followed this journey, perhaps many thoughts have arisen while transforming
the original code.
- Was it necessary to convert the original code to a functional style?
- Should the transformation have stopped after one or two steps?
- Is the resulting functional code more readable? less?
- Which code would you rather see in your code base? Why?
- Is the effort to learn the functional style of JavaScript worthwhile?
- If you saw this code in a code review, how would you react?
- The pluckWhere() function contains only two statements with each calling a single underscore function. Is this overkill? Was this necessary?
- Is this a new way of thinking, or just a way to obscure code with lots of unnecessary function calls?
- All the additional functional calls create additional overhead. How much performance does this cost? Checkout this video
- Is functional programming just a trend that will blow away soon?
- Does functional programming liberate us from the von Neumann style?
- Do the usual claims hold water?
- It’s the future, the next big thing.
- Changes your thinking. Another tool to have.
- Shorter, terser code
- More power, better abstractions, convenient
- No side-effects
- Reliable, proven
- Is this example just an example? Any real world relevance?
- Is shorter really better?
- Any studies to prove the claims? Lots of papers on FP exist, but real-world studies?
- A common saying of FP: “…makes you think differently”. Is this worthwhile?
- Is FP too weird?
- Is FP out of your comfort zone? Out of your experience zone?
- Are the absence of side-effects desirable?
- Does FP allow easier reasoning about programs?
- Does the less state in FP mean less worries?
Summary and Advice
This presentation demonstrates an application of functional thinking when refactoring JavaScript code. We began with a simple function and transformed it according to the following rules:
- Use Functions Instead of Simple Values
- Model Data Transformations as a Pipeline
- Extract Abstract Functions
The refactored code is far superior to the original. Do you agree? It is more extensable, has no mutable state, and no if statements.
Functional refactoring techniques come only after study and practice. Start with some simple techniques easily understandable and work from there. Be careful not to create results not easiliy understood.
When first starting to learn this process, start gradually. An initial approach would eliminate looping structures to functional loops such as _.each(), _.map() and apply(). Then consider refactoring conditionals into functions. Obviously studing examples from the references below greatly accelerates the process. Above all, do not attempt too much too soon!
The web contains plenty of material on this process. Good Luck!
References
Highly recommended books:
- JavaScript Allonge by Reginald Braithwaite. An online free version: JavaScript Allonge
This is truly a mind-blowing book explaining programming with funcitons. This book starts with the basics of functions and builds and builds. It teaches how to handle complex code without dumbing it down. This book uses ES6. 438 pages of eBook – I wish there were a paperback edition! - Functional JavaScript by Michael Fogus. A classic every JavaScript programmer should become familiar with. The author begins with the popular underscore library and builds functional programming techniques by applying underscore functions. This illustrates how to use functional techniques as opposed to a straight-forward description of underscore methods.