Why FP?

How functional thinking can help you write better software

What is Functional Programming

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

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);

Concepts

In no particular order:

  • Higher Order Functions
  • Purity
  • Laziness

Higher Order Functions

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

Immutable data structures

Objects whose state cannot be changed after creation. This helps mitigate against bugs.

Immutable data structures

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];

Immutable data structures

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

 

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.

Pure Functions

 

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);
        });
};

Pure Functions

 

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.

Pure Functions

 

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.

Pure Functions

 

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

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

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.

Lazy Evaluation

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());

Further Reading

  • Why Functional Programming Matters:
    https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf
  • Pure Functions:
    http://www.sitepoint.com/functional-programming-pure-functions/
  • Immutable JS:
    http://facebook.github.io/immutable-js/docs/#/
  • Stream JS: http://streamjs.org/