Functional Programming with JavaScript

presented by Bill Kidwell

for the KY JS Users Group

Let's stick with a simple definition

Functional programming is about functions without side effects

Definition adapted from Mary Rose Cook

A pure function is a function which

  • Given the same input, will always return the same output (deterministic)
  • Produces no side effects (doesn't change external state)
  • Relies on no external mutable state

Impure increment function


                    var a = 0;

                    function increment() {
                        a+=1;
                    }
                

Pure increment function


                    function increment(a) {
                        return a+1;
                    }
                 

Ramda.js

A practical functional library for JavaScript programmers.

http://ramdajs.com

The name?

They like sheep.

Some fundamentals

map

  • similar to forEach
  • iterates over an array
  • calls the function on each element
  • returns results in a new array
  • think of mapping from one coordinate space to another

Some fundamentals

map


                    const double = x => x * 2;
                    R.map(double, [1,2,3]); //=> [2,4,6]
                    [1,2,3].map(double); //=> [2,4,6]
                

... or map over an object


                    const data = { a: 1, b: 2, c: 3 };
                    R.map(double, data);  //=> { a: 2, b: 4, c: 6 }
                

Some fundamentals

reduce

  • from a list, to a single result
  • commonly used for aggregates (max, min, sum)

An example with reduce


                    const array = [1,2,3];
                    const sum = R.reduce(R.add, 0, array); // => 6
                
var acc = 0;
acc = R.add(acc, array[0]); // => 0 + 1 = 1
acc = R.add(acc, array[1]); // => 1 + 2 = 3
acc = R.add(acc, array[2]); // => 3 + 3 = 6
return acc; // => 6

Practice with reduce


                    // R.reduce( function, accumulator, list)
                    const sum = R.reduce(R.add, 0, array); 
                

Find the max of an array using reduce and R.max(a,b)


                    const max = R.reduce(R.max, Number.MIN_VALUE, array);
                

Find the min of an array using reduce and R.min(a,b)


                    const min = R.reduce(R.min, Number.MAX_VALUE, array);
                

So what?

Are map and reduce really better than a for loop?

One place they shine is with filter/reject

filter/reject


                    const isEven = x => x % 2 === 0;

                    R.filter(isEven, [1, 2, 3, 4]); // => [2, 4]
                

                    R.reject(isEven, [1, 2, 3, 4]); // => [1, 3]
                
category, customer, amount Elite, Eric, 34.99 Delux, Doug, 39.99 Delux, Drake, 39.99 Lite, Lewis, 29.99 Elite, Eleanor, 34.99 Delux, Dakota, 39.99 Plus, Paul, 24.99 Elite, Ellen, 34.99 Basic, Bob, 12.99 Delux, David, 39.99 Lite, Linda, 29.99 Plus, Pauline, 24.99 Delux, Diego, 39.99 Lite, Lenny, 29.99 Elite, Edward, 34.99

                      const sales = [
                        { category: "Elite", customer: "Eric", amount: 34.99 },
                        { category: "Delux", customer: "Doug", amount: 39.99 },
                        { category: "Delux", customer: "Drake", amount: 39.99 },
                        { category: "Lite", customer: "Lewis", amount: 29.99 },
                        { category: "Elite", customer: "Eleanor", amount: 34.99 },
                        { category: "Delux", customer: "Dakota", amount: 39.99 },
                        { category: "Plus", customer: "Paul", amount: 24.99 },
                        { category: "Elite", customer: "Ellen", amount: 34.99 },
                        { category: "Basic", customer: "Bob", amount: 12.99 },
                        { category: "Delux", customer: "David", amount: 39.99 },
                        { category: "Lite", customer: "Linda", amount: 29.99 },
                        { category: "Plus", customer: "Pauline", amount: 24.99 },
                        { category: "Delux", customer: "Diego", amount: 39.99 },
                        { category: "Lite", customer: "Lenny", amount: 29.99 },
                        { category: "Elite", customer: "Edward", amount: 34.99 }
                      ];                    
                

Let's filter this list by category


                    const firstSale = sales[0], secondSale = sales[1];

                    const isCategory = (cat, object) => 
                                        R.prop('category', object) === cat;
                

                    isCategory('Elite', firstSale);  // => true
                    isCategory('Elite', secondSale); // => false
                

What about predicates for all of our categories?

Spice it up with a little curry


                    const isCategory = R.curry(
                        ( cat, object ) => R.prop('category', object) === cat
                        );
                

                    const isElite = isCategory("Elite");
                    const isDelux = isCategory("Delux");
                

                    isElite(firstSale); // => true
                    isDelux(firstSale); // => false
                

Manually currying our function


                    const isCategory = function(cat) {
                        return function (object) {
                            return R.prop('category', object) === cat;
                        }
                    }

                    const isElite = isCategory('Elite');
                    isElite(firstSale); // => true
                    isElite(secondSale); // => false
                

Back to filtering...


                   const eliteSales = R.filter(isElite, sales);
                   // => [
                   //   { category: "Elite", customer: "Eric", amount: 34.99 },
                   //   { category: "Elite", customer: "Eleanor", amount: 34.99 },
                   //   { category: "Elite", customer: "Ellen", amount: 34.99 },
                   //   { category: "Elite", customer: "Edward", amount: 34.99 } 
                   // ]
                
                   
                   const deluxSales = R.filter(isDelux, sales);
                   // => [
                   //   { category: "Delux", customer: "Doug", amount: 39.99 },
                   //   { category: "Delux", customer: "Drake", amount: 39.99 },
                   //   { category: "Delux", customer: "Dakota", amount: 39.99 },
                   //   { category: "Delux", customer: "David", amount: 39.99 },
                   //   { category: "Delux", customer: "Diego", amount: 39.99 }
                   // ]
                 

What can we calculate with these lists?


                    // # of sales
                    R.length(eliteSales); // 4
                

                    // Total of sales
                    const getSalesAmounts = R.map(R.prop("amount"));
                    const eliteSalesAmounts 
                    = getSalesAmounts(eliteSales); // => [34.99, 34.99, 34.99, 34.99]

                    const sum = R.reduce(R.add, 0);
                    sum(eliteSalesAmounts); // 139.96
                

All together now


                        // Filter the array to the items with that category
                        const filterByCategory = 
                                (category, objects) => R.filter(
                                    isCategory(category), objects);

                        // Get the array of sale amounts
                        const getSalesAmounts = R.map(R.prop("amount"));

                        // And sum it
                        const sum = R.reduce(R.add, 0);                        
                    

                        const calculateTotal = R.pipe(
                            filterByCategory,
                            getSalesAmounts,
                            sum
                        );
                    

Let's break that down...


                    calculateTotal("Elite", sales); 
                

                        var result1 = 
                            filterByCategory("Elite", sales) // => List of all elite sales
                    

                        var result2 = getSalesAmounts(result1); // => Array of all amounts
                    

                        sum(result2); // => 139.96
                    

                        //Which is equivalent to...
                        sum( getSalesAmounts( filterByCategory("Elite",sales) ) );
                    

How can I get all of the results at once?


                        R.groupBy( R.prop("category"), sales );
                    

                        {
                            Basic: [
                                { category: "Basic", customer: "Bob", amount: 12.99 }
                            ],
                            Delux: [
                                { category: "Delux", customer: "Doug", amount: 39.99 }, ...
                            ],
                            Elite: [
                                { category: "Elite", customer: "Eric", amount: 34.99 }, ...
                            ],
                            ...
                        }
                    

                    // Group sales by category
                    const groupSales = R.groupBy(R.prop("category"))

                    // For a category, sumPrices
                    const sumPrices = R.pipe( getSalesAmounts, sum );

                    // In one function
                    const allTotals = R.pipe(
                        groupSales, 
                        R.map( sumPrices )
                    )

                    allTotals(sales);
                

                    {
                        Basic: 12.99,
                        Delux: 199.95000000000002,
                        Elite: 139.96,
                        Lite: 89.97,
                        Plus: 49.98
                    }                    
                

"zip" Objects


                    var labelArray = ['Basic', 'Plus', 'Lite', 'Elite', 'Delux'];
                    var valueArray = [1,2,3,4,5];
                    
                    var result = R.zipObj(labelArray, valueArray);                
                

                    { 
                      "Basic": 1,
                      "Plus":  2,
                      "Lite":  3,
                      "Elite": 4,
                      "Delux": 5
                    }
                

Problem statement

  • Input is an object, where each value is an array
  • Returns an array of objects
  • Each object has the keys of the original object
  • The length of the array is == shortest array

Step 1: Get the Lengths of the Arrays


                    const getLengths =
                        arrayObj => R.map(R.length, arrayObj);
                

                    var arrayData = {
                        label: ["Basic", "Plus", "Lite", "Elite", 
                                "Delux", "Oops", "Uh Oh"],
                        value: [1, 2, 3, 4, 5],
                        color: d3.schemeSet1
                    };

                    getLengths(arrayData); // => { label: 7, value: 5, color: 10 }
                

Step 2: Find the length of the smallest array


                        // To get the values of the object
                        R.values(lengthObj); // => [7,5,10]

                        const getMinOfObject = 
                            lengthObj => 
                                R.reduce(R.min, Number.MAX_VALUE, R.values(lengthObj));

                        // => 5
                    

Speaking of objects...


                        const obj = {a: 2, b: 4, c: 6};
                    

                        // Get the keys
                        R.keys(obj); // => ['a', 'b', 'c']
                    

                        // Get the values
                        R.values(obj); // => [2, 4, 6]
                    

Step 3: Build an index array

Instead of determining which array was smallest...


                    R.range(0, 5); // => [0,1,2,3,4]

                    const getIndexes 
                        = R.pipe( 
                            getLengths, 
                            getMinOfObject, 
                            R.range(0)
                        );
                

Step 4: Get the nth value of each array


                    const getNth = R.curry( 
                        (arrayObj, n) => R.map( R.nth(n), arrayObj ) 
                    );

                    getNth(arrayData, 0); 
                    // => {label:"Basic", value: 1, color:"#e41a1c"},
                

As a single function


                    function zipArrays( arrayObj ) { 
                        var indexes = getIndexes(arrayObj);
                        return R.map(getNth(arrayObj), indexes);
                    }                                              
                

Conclusions

  • The barrier to entry is low
  • You probably use aspects of it today
  • Start by using pure functions when you can
  • map and reduce are a good starting point

Recommended Reading

Randy Coulman's series Thinking in Ramda

Eric Elliot's series Composing Software

Professor Frisby's Mostly Adequate Guide to Functional Programming by Brian Lonsdorf (aka Dr. Boolean)