Functional Refactoring in JavaScript
Join the DZone community and get the full member experience.
Join For FreeWhen discussing functional programming we often talk about the machinery, and not the 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) } }
The averageSalary
function is what we are going to refactor.
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 }
The function 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.
This function can be used as follows.
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 we got is convoluted, not to mention hard to extend. If I just added another condition, the signature of the function (thus, the public interface) would have to change, and the if statement would grow into a real monster.
Let’s apply some functional programming techniques to refactor this function.
Use Functions Instead of Simple Values
Using functions instead of simple values may seem counterintuitive at first, but it is actually a very powerful technique for generalizing code. In our case it means replacing the minSalary
and department
parameters with two functions checking the conditions.
function averageSalary(employees, salaryCondition, departmentCondition){ var total = 0 var count = 0 _.each(employees, function(e){ if(salaryCondition(e) && (departmentCondition == undefined || departmentCondition(e))){ total += e.salary count += 1 } }) return (count == 0) ? 0 : total / count } .... expect(averageSalary(empls, function(e){return e.salary > 50}, sales.works)).toEqual(150)
What we have done is that we have unified the interfaces of the salary and department conditions. Whereas before both the conditions were implemented ad-hoc, now they are explicitly defined and conform to the same interface. This unification allows us to pass all the conditions as an array.
function averageSalary(employees, conditions){ var total = 0 var count = 0 _.each(employees, function(e){ if(_.every(conditions, function(c){return c(e)})){ total += e.salary count += 1 } }) return (count == 0) ? 0 : total / count } ... expect(averageSalary(empls, [function(e){return e.salary > 50}, sales.works])).toEqual(150)
Since an array of conditions is nothing but a composite condition, we can pull out a simple combinator making it explicit.
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(e){ if(and(conditions)(e)){ total += e.salary count += 1 } }) return (count == 0) ? 0 : total / count }
It is worth noting that the and
combinator is generic, and, therefore, can be reused and potentially extracted into a library.
Intermediate Results
The averageSalary
function has already got more robust. A new condition can be added without breaking the interface of the function or changing its implementation.
Model Data Transformations as a Pipeline
Another useful practice of functional programming is modeling all data transformations as a pipeline. Which in our case means extracting the filtering out of the loop.
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 }
This change made the counting unnecessary.
function averageSalary(employees, conditions){ var filtered = _.filter(employees, and(conditions)) var total = 0 _.each(filtered, function(e){ total += e.salary }) return (filtered.length == 0) ? 0 : total / filtered.length }
Next, if we pluck the salaries before adding them up, the summation will become a simple reduce.
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 }
Extract Generic Functions
The next observation is that the last two lines have nothing to do with our domain. There is nothing there about employees or departments. It is essentially an implementation of the average function. So let’s make it explicit.
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') return average(salaries) }
Once again, the extracted function is absolutely generic.
Finally, after pulling out the plucking of salaries, we get our final solution.
function employeeSalaries(employees, conditions){ var filtered = _.filter(employees, and(conditions)) return _.pluck(filtered, 'salary') } function averageSalary(employees, conditions){ return average(employeeSalaries(employees, conditions)) }
Comparing the original and final solutions I can say without a doubt that the latter is far superior. First, it is more generic (we can add new types of conditions without breaking the interface of the function). Second, we got rid of the mutable state and if statements, which made the code easier to read and understand.
Up to Eleven
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.
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')
Summing Up
In this article I showed how to apply functional thinking when refactoring JavaScript code. I did that by taking a simple function and transforming it using the following rules:
- Use Functions Instead of Simple Values
- Model Data Transformations as a Pipeline
- Extract Generic Functions
The refactored function is far superior to the original. It is more extendable, has no mutable state, and no if statements.
Read More
Highly recommend checking out the following two books:
Published at DZone with permission of Victor Savkin, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments