How functional thinking can help you write better software
At its most basic, functional programming is a paradigm that allows functions to be treated as inputs to and outputs from other functions, so called "higher-order" functions.
Higher order functions are functions that accept other functions as input, or generate other functions as output.
You're probably already using them.
setTimeout(function() { console.log("So this is it?") }, 1000);
setTimeout(function() { console.log("Yep") }, 2000);
In no particular order:
var auditTrail = [
{timestamp: 1, transactionValue: 10},
{timestamp: 2, transactionValue: 1},
{timestamp: 30, transactionValue: 2000},
]
var imperativeGetAllByTimestamp = function(timestamps) {
var results = [];
for(var i = 0; i < db.length; i++) {
if(timestamps.indexOf(auditTrail[i].timestamp) > -1) {
results.push(db[i]);
}
}
return results;
};
var functionalGetAllByTimestamp = function(timestamps) {
return auditTrail.filter(
function(item) {
return (timestamps.indexOf(item.timestamp) > -1);
});
};
Can lead to more concise code
Objects whose state cannot be changed after creation. This helps mitigate against bugs.
var auditTrail = [
{timestamp: 1, transactionValue: 10},
{timestamp: 2, transactionValue: 244},
{timestamp: 30, transactionValue: 300},
];
var getTransactionsByTimestamp = function(timestamp) {
return auditTrail.filter(
function(item) {
return (timestamps.indexOf(item.timestamp) > -1);
});
};
var otherFunctionDefinedElsewhere() {
// Do useful stuff
auditTrail[2].transactionValue = 10;
// Do other useful stuff
}
var transactions = getTransactionsByTimestamp([1, 30])
.map(function(item) { return item.id; });
console.log(transactions);
>>> [10, 10];
var auditTrail = Immutable.List([
{timestamp: 1, transactionValue: 10},
{timestamp: 2, transactionValue: 244},
{timestamp: 30, transactionValue: 300},
]);
var getTransactionsByTimestamp = function(timestamp) {
return auditTrail.filter(
function(item) {
return (timestamps.indexOf(item.timestamp) > -1);
});
};
var otherFunctionDefinedElsewhere() {
// Do useful stuff
auditTrail[2].transactionValue = 10;
// Do other useful stuff
}
var transactions = getTransactionsByTimestamp([1, 30])
.map(function(item) { return item.id; });
console.log(transactions);
>>> [10, 300];
Pure functions produce the same result whenever they're run against the same arguments.
A related concept is that of referential transparency, where an expression can be replaced with its value without affecting the behaviour of the program.
Taken together these yield interesting performance optimizations.
var auditTrail = Immutable.List([
{timestamp: 1, transactionValue: 10},
{timestamp: 2, transactionValue: 244},
...
{timestamp: 30, transactionValue: 300},
]);
var getTransactionsByTimestamp = function(timestamp, auditTrail) {
// We now pass the auditTrail as a parameter since purity relies on
// all state being explicit
return auditTrail.filter(
function(item) {
return (timestamps.indexOf(item.timestamp) > -1);
});
};
Since we've transformed getTransactionsByTimestamp into a pure function, we know that given the same audit trail, and the same list of timestamps, the result will always be the same. We can take advantage of this fact to memoize the function.
var auditTrail = Immutable.List([
{timestamp: 1, transactionValue: 10},
{timestamp: 2, transactionValue: 244},
...
{timestamp: 30, transactionValue: 300}
]);
var getTransactionsByTimestamp = function(timestamp, auditTrail) {
// We now pass the auditTrail as a parameter since purity relies on
// all state being explicit
// Performance would be O(n) (linear time) because of the call to filter()
return auditTrail.filter(
function(item) {
return (timestamps.indexOf(item.timestamp) > -1);
});
};
var memoize = function (fn) {
var cache = {};
return function() {
var args = arguments.slice();
if (args in cache)
return cache[args];
else
return (cache[args] = fn.apply(this, args));
}
};
var memoizedGetTransactionsByTimestamp = memoize(getTransactionByTimestamp);
// Initial performance is O(n), but subsequently O(1) - constant time, for the
// same set of arguments.
A further optimization would be to replace calls to the function with the actual return value during a pre-processing/compilation step. We can do this confidently since we know the value won't change during runtime, saving us even more computation cycles.
Lazy evaluation is a feature of functional programming that enables holding off on evaluating an expression until the value is needed, with interesting results. e.g. operations on an infinitely long list are possible, since we only take the elements that are needed.
Lazy evaluation is a feature of functional programming that enables holding off on evaluating an expression until the value is needed, with interesting results. e.g. operations on an infinitely long list are possible, since we only take the elements that are needed. There are several implementations of lazy lists in JS. We'll go with streamjs.org for the purposes of this demonstration.
var auditTrail = Stream(
{timestamp: 1, transactionValue: 10},
{timestamp: 2, transactionValue: 244},
...
{timestamp: 3000000, transactionValue: 23}
);
var getTransactionsByTimestamp = function(timestamps, auditTrail) {
return auditTrail.filter(
function(item) {
return (timestamps.indexOf(item.timestamp) > -1);
});
};
var transactionsBetween2000And3000 = getTransactionsByTimestamp(
Stream.range(2000, 3000),
auditTrail);
// The actual filtered set is not generated until it is called, as would be the case with
// a standard array. This means that auditTrail can hold far more items than a standard array,
// and allows us
transactionsBetween2000And3000.map(doSomethingWithEachItem());