In continuing this series of posts about functional JavaScript (one, two), I whimsically wondered if we could apply the SOLID principles of object-oriented programming. We took a look at S last time (the Single Responsibility Principle), and were fairly successful. The principle I introduced there was not only that the functions we write should do one thing and do it well. If we can embrace global immutability, so much the better (in other words, the function should not have side effects). Small functions of this type are also well worth writing since they help document the code via their names. It’s now time to look at O, the Open/Closed Principle.
I’d have to say that the Open/Closed Principle is possibly the more difficult of the principles to grasp. Bertrand Meyer first proposed it way back when in 1988: “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”. In object-oriented terms, we read this as the classes we write should not be modified, instead we should introduce changes and extensions to the code’s behavior through inheritance. In fact, these days, with a shift away from traditional implementation inheritance, we use compositional inheritance through interfaces as being a better model for OOP. The thing that causes people to falter with the Open/Closed Principle is that it seems to preclude fixing bugs in the base classes we write (they’re supposed to be closed for modification, after all). This issue is possibly another bonus point for compositional inheritance, since the interfaces we write have no implementation to get wrong.
Anyway, what does this mean for functional programming? Meyer was prescient enough nearly 30 years ago to include functions in his definition, but what can that mean for us? Given an existing function we cannot change, what does it mean to extend the function?
In fact, all we need to do is take a look at the Array object in JavaScript to see some examples of functions that we can extend. They are standard higher-order functions for arrays expressed in JavaScript terms. The two major ones, if I may call them that, are Map and Reduce. Map applies a given function to each element of an array to produce another array, leaving the original array intact (that’s the immutability principle in action). Reduce applies a given function to each element of an array in order to calculate a single value. (In functional programming terms, Reduce is an example of the Fold function.) There are several other such higher-order functions as well: filter, every, some, forEach, find, and so on.
The interesting thing to experiment with in functional programming terms is to replicate these “lesser” higher-order array functions with the main ones. It’s a set of great exercises in thinking and writing functionally in JavaScript.
To quote MDN, the filter()
method creates a new array with all elements that pass the test implemented by the provided function. The syntax is:
arr.filter(callback[, thisArg])
To remind you, the syntax of reduce()
is:
arr.reduce(callback[, initialValue])
I would fire up your editor of choice and see whether you can do this exercise before seeing the answer I have. You should write a function that looks like this:
function filter(arr, callback) {
// your code that uses arr.reduce()
}
where arr
is the array to be examined, callback
is the function that, given an element, returns true
to signal the element is to be included in the returned array. This callback function accepts three parameters: element
for the current element in the array, index
for the index of that element, and array
for the array being filtered.
function filterTest(element, index, array) {
// return true if the element is to be included
}
The new filter()
function should return a new array containing those elements that passed the test. OK, go try it out before reading any further!
It turns out that the answer is relatively simple providing you realize that the value calculated by reduce()
doesn’t have to be a simple type like number or string. It can be an array; in which case, the initial value passed to reduce()
is an empty array.
function filter(arr, callback) {
"use strict";
return arr.reduce(function (resultArray, element, index, array) {
if (callback(element, index, array)) {
resultArray.push(element);
}
return resultArray;
}, []);
}
The return value from this filter()
function is the return value from calling the reduce()
function with a callback function that itself calls the passed in callback function to determine whether to add the current element or not to the array being built up. By the way: bonus points if you managed to incorporate the optional thisArg
parameter from the original filter()
syntax.
Easy exercise for the reader: having implemented the filter()
function using reduce()
, I’m sure you can easily implement map()
using reduce()
as well.
To quote MDN, every()
tests whether all elements in the array pass the test implemented by the provided function. The syntax is:
arr.every(callback[, thisArg])
The callback function accepts three parameters: element
for the current element in the array, index
for the index of that element, and array
for the array being tested.
function testValid(element, index, array) {
// return true if the element passes the test
}
The interesting thing here is that every()
should immediately returns false if it encounters an element that returns false from the callback. The function to write has this syntax:
function every(arr, callback) {
// code that calls reduce()
}
where the callback has the syntax already shown. The naïve answer would be:
function every(arr, callback) {
"use strict";
return arr.reduce(function (result, element, index, array) {
return result && callback(element, index, array);
}, true);
}
But unfortunately reduce()
has no “exit immediately” option: it applies the callback function to every element in the array. So, if the first element in a 100,000 element array fails the test, reduce will continue to run through the other 99,999 elements unnecessarily.
In fact, this is an example of a case where reduce()
is so closed that we can’t extend it in the manner we want. We’d have to break the internal “loop” processing of reduce()
by throwing an exception:
function every(arr, callback) {
"use strict";
try {
arr.reduce(function (result, element, index, array) {
if (!callback(element, index, array)) { throw "testfail"; }
return result;
}, true);
}
catch (e) {
if (e === "testfail") { return false; }
throw e;
}
return true;
}
Which is enough to make me run screaming from the room.
I hope that this discussion (and exercises!) has helped cement your thoughts about functional programming in JavaScript, especially with regard to the Open/Closed Principle. Next time, we shall look at the L in SOLID, the Liskov Substitution Principle, in regard to functional programming.
3 Responses
#1 J.P. said...
16-Feb-16 6:27 AMI would argue that the Open-Closed Principle has nothing to do with inheritance. It's more about writing our code in such a way that working code is not modified when we are adding new features/behavior. Many design patterns will lead you down this road. The simplest example I can think of a switch statement. If we need a new condition, it would have to be modified (therefore this bit of code is not 'closed'). Perhaps this switch statement could be refactored to a Strategy pattern...let's say so. Then extending the behavior would mean writing a new strategy, leaving the core handling untouched. Thus, the core code which acts upon the strategy would remain untouched...it's closed.
#2 Jeff M said...
22-Feb-16 6:30 PMI feel like you were close to addressing a difficult topic, but then you veered on a tangent. You spent the rest of the article re-implementing a handful of array functions in terms of reduce. Okay, that's nice, but... what if tomorrow you discover a bug? After all, that's the issue you raised but never addressed. What if tomorrow you discover a bug in your filter function? Do you fix it? But that means your function is open to modification. Do you make a new filter2 function? And so on for every bug fix? filter3, filter4? It's an interesting problem, and it's nice that you asked the question, but it seems like you forget to offer an answer.
#3 YSharp said...
08-Mar-16 3:45 PMVery nice article.
Indeed, "reduce" in JavaScript (and others) is quite powerful. Here is a proof of concept which uses it to mimic XSLT (1.0)'s processing model:
http://programmers.stackexchange.com/a/311736/189934
Or also (using the same) how to embed the XML infoset within JSON, for round-trippable mappings:
http://stackoverflow.com/a/35810403/1409653
Cheers,
Leave a response
Note: some MarkDown is allowed, but HTML is not. Expand to show what's available.
_emphasis_
**strong**
[text](url)
`IEnumerable`
* an item
1. an item
> Now is the time...
Preview of response