FP in ES6 Part I: Building Blocks of FP

node v6.17.1
version: 1.0.0
endpointsharetweet
Mathematical functions and how they relate to programming functions
// name: squaring function // formula: f(x) = x² // symbolic: f: x → x² // domain (from): ℝ (the set of real numbers) // codomain (to): ℝ (the set of real numbers) const f = x => x * x; // <-- equivalent to var f = function(x) { return x * x; }; f(3.14159); // 9.869587728099999 // Note: If we give this function the wrong type, it'll explode. That's fine, we're ignoring that for now!
// name: successor function // formula: g(n) = n + 1 // symbolic: g: n → n + 1 // domain (from): ℤ (the set of integers) // codomain (to): ℤ (the set of integers) const g = n => n + 1; // <-- equivalent to var g = function(n) { return n + 1; }; g(4); // 5
Composition of mathematical functions
// composition of our squaring and successor functions // f(g(n)) = (n + 1)² f(g(4)); // 25
// composition of our successor and squaring functions // g(f(x)) = x² + 1 g(f(4)); // 17
// a naive compose implementation const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)));
f(g(4)) === compose(f, g)(4); // true - 25
const squarePlusOne = compose(g, f); // we can bind composed functions to a variable! g(f(4)) === squarePlusOne(4); // true - 17
Function application is function invocation
// function application const cubeIt = n => n * f(n); cubeIt(3); // 27
cubeIt(3) === cubeIt.apply(undefined, [3]); // true - (immediately invoked)
cubeIt(3) === cubeIt.call(undefined, 3); // true - (also immediately invoked)
// first simple functions that take 2 arguments const multi = (a, b) => a * b; // multiplies two numbers const adder = (a, b) => a + b; // add two numbers
// partially applying 2 to multi with bind() const double = multi.bind(undefined, 2); // NOT invoked, but bound to the name double double(5);
// equivalent! double(5) === multi(2, 5);
// also equivalent! const dbl = n => multi(2, n); // same thing as multi.bind using ES6 arrow syntax double(5) === dbl(5);
Pure & impure functions
// impure function // notice how let gives us a clue that we're doing something impure here! let currentYear = new Date().getFullYear(); let upperLimit = 15; let myYearRange = []; const impureYearRange = (year) => { upperLimit = upperLimit+1; const spread = [...Array(upperLimit).keys()]; currentYear = parseInt(year.toString().substr(2), 10); spread.forEach(yr => myYearRange.push(yr + currentYear)); }; impureYearRange(currentYear); myYearRange; // [17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32] // I'm calling this out as impure, but the rest are up to you! :)
Note about [...Array(upperLimit).keys()]: I stumbled across this construct while trying to throw together a quick & dirty range function in the Babel REPL at http://babeljs.io/repl. If you just do ...Array(15).keys() you get a syntax error (which makes sense - you wouldn't use the spread operator outside of a function or array/object literal). BUT WAIT! Why can I do ...foo as a function parameter? Probably because under the hood, JS arguments are thrown into an "Array-like object". See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments (Function.prototype.arguments). If you do Array(15).keys() you're just going to get an iterator back. If you do Array(15).keys() inside of a function call like console.log(), you get to see the values but can't iterate over them. To get what we want, we need to apply the spread operator to an iterable object (a throw-away Array instance) of the size we need, then get the keys off of that object and fill an array literal with those values. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator Also note that this is just a quick & dirty range function and isn't really important for understand what the talk is about 😜 and this syntax is not going to be supported in future versions (IIRC) because I think they're going to go in a different direction with comprehension-style syntax (todo: look up what's going on with that). Take a look at ramda's range to see a really robust implementation: https://github.com/ramda/ramda/blob/master/src/range.js
// Comparison of ES6 homebrewed range() and generated ES5 code // this function is generated by babel function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] const rangeA = [...Array(20).keys()]; // ES6 // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] const rangeB = [].concat(_toConsumableArray(Array(20).keys())); // ES5 - generated by babel
rangeA;
rangeB;
currentYear; // 17
upperLimit; // 16 (upperLimit was mutated!)
/** Demonstrates: * * closure * * lambda * * first-class functions * * higher-order functions */ const newYearRange = (lim=15) => { const spread = [...Array(lim+1).keys()]; const currentYear = parseInt(new Date().getFullYear().toString().substr(2), 10); return spread.map(num => num + currentYear); }; // same results as above - only without mutating any external state newYearRange(); // [17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32] // Is this function pure? Why/why not?
// Return a new data structure, rather than mutating an existing one! const userRecord = { name: 'Neville Sinclair', occupation: 'Adventurer Extraordinaire', id: '67b39266-f7f7-4d52-9bbb-c0e5e718be84' }; const addAgeToUser = (user, age) => { // merges the existing data with the new data, // returning the merged data without altering the original return Object.assign({ age: age }, user); // using Object.assign() //return { age: age, ...user }; // using ES7 spread operator // for shallow copies! :) Eric }; userRecord;
addAgeToUser(userRecord, 37); // new object!
userRecord; // original is unchanged!
Partial application & currying
/** A generic year range function from a year to n years, inclusive * @param {Date} currentDate=new Date() - the current date * @param {Number} limit=15 - upper limit of the range * @returns {[Number]} - an Array of 2-digit years */ const yearRange = (currentDate = new Date(), limit = 15) => { const lastTwo = parseInt(currentDate.getFullYear().toString().substr(2), 10); return [...Array(limit+1).keys()].map(year => lastTwo + year); }; // same result again, using default values yearRange(); // [17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32] // Is this pure? Why/why not?
/** A n-year range function from 1970 * @param {Number} limit - upper limit of the range * @returns {[Number]} - an Array of 2-digit years */ const customYearRange = yearRange.bind(null, new Date(1970, 1, 1)); // equivalent to: // const customYearRange = (limit) => yearRange(new Date(1970, 1, 1), limit); // get 1970 plus 25 years (25 years from 1970, inclusive) customYearRange(25); // [70, 71, 72, 73, 74, 75, 76, 77, 78, 79, …] // Is this pure? Why/why not?
// simple currying for functions with an arity of 2 (binary functions) // ARGUMENT ORDER IS IMPORTANT! const curry = fn => a => b => fn(a, b); // curried version of multi const cmulti = curry(multi); // multi(a, b) === cmulti(a)(b) multi(12, 9) === cmulti(12)(9);
// a couple of new functions, using our curried multi const timesTwelve = cmulti(12); // creates multiples of 12 const dubs = cmulti(2) // doubles its input
timesTwelve(9); // 108
// binary function is equivalent to our curried function, given the same inputs! multi(12, 9) === timesTwelve(9);
const cadder = curry(adder); // curried version const addNine = cadder(9); // add 9 to whatever addNine(dubs(9)); // 27 - curried functions can be composed!
// we can also curry an inline anonymous function const greeter = curry((interval, name) => `Good ${interval}, ${name}!`); const goodMorning = greeter('morning'); // now, we can say good morning to people goodMorning('Carlo'); // Good morning, Carlo!
// redefining our multi() function inline const mult = curry((a, b) => a * b); // redefining our squaring function using our curried function const square = n => mult(n)(n); const fourSquare = () => square(4); fourSquare();
// defining a cube function const cube = n => mult(n)(square(n)); cube(9);
More practice with composition - revisiting our yearRange() function
/** * Problem statement: a function that takes a starting year and generates a * list of 2-digit years up to a given limit, including the current year * * Let's think about what we want to do here (one possible solution): * 1. Take a date and return the last two digits of that date's year * 2. Generate a list of numbers from 0 to our upper limit * 3. Return the list of 2-digit years by mapping sum over our data * * Assuming that's the general algorithm we want to use to solve this problem * (we could solve this in any number of ways, but are taking that issue off the * table for the purposes of the exercise), how might we go about it in a more functional way? */
The benefit of functional utility libraries like lodash or ramda is that they provide these utility functions in a well-written, well-tested, well-documented library. For now, we're going to roll our own.
// first, some simple, general-purpose utility functions // redefining our adder() above as a curried function with a saner name const sum = curry((a, b) => a + b); // redefining our successor function above using our curried sum() - returns n + 1 const successor = increment = sum(1); // alias increment to successor // use our increment function to make our own simple range function // returns [0, 1, 2, ..., limit] const range = limit => [...Array(increment(limit)).keys()];
Using collection pipelines to write more functional Javascript (more about collection pipelines at https://martinfowler.com/articles/collection-pipeline/) Note: This idea is being used here as a bridge from the fundamentals of functional programming to category theory. Not that it's not a useful technique generally, but what I'm using it for here is to get across the cognitive gap between Part I and Part II of this talk in what is hopefully a graceful way.
// An initial refactoring... // if we *really* wanted to do this in a single function, we could do: // Is this pure? Why/why not? const listOfYears = (date = new Date(), limit = 15) => [date] .map(d => d.getFullYear()) .map(y => y.toString()) .map(y => y.substr(2)) .map(y => parseInt(y, 10)) .map(y => range(limit) .map(n => sum(n)(y))) .reduce(xs => xs); listOfYears(); // this is called a collection pipleline - cool, but it's still doing too much work... // two distinct jobs that we might want to do later, so let's split them up!
/** Returns the last two digits of the year on the date object passed in * @param {Date} - an instance of Date() * @returns {Number} - a 2-digit representation of the date instance's year */ // Is this pure? Why/why not? const twoDigitYear = date => // take any date - our caller shouldn't care! [date] // first, throw the date into an Array .map(d => d.getFullYear()) // now, we can map() and reduce()! .map(y => y.toString()) // turn full year into a String .map(y => y.substr(2)) // get the last two characters .map(y => parseInt(y, 10)) // parse the string as an int with a radix of 10 .reduce(y => y); // reduce on the identity function to return the value // does the same thing as this line (line 87 above): // const lastTwo = parseInt(currentDate.getFullYear().toString().substr(2), 10); // but is more declarative and composable, not to mention easier to read!
// using partial application to create a thisYear() function, // so we can generate the current year's 2-digit abbreviation somewhere else later! // equivalent to const thisYear = twoDigitYear.bind(undefined, new Date()); // Is this pure? Why/why not? const thisYear = () => twoDigitYear(new Date()); thisYear(); // What about now? Is this okay? const thisYear2 = twoDigitYear(new Date()); // ... do stuff with thisYear
/** Generate a list of 2-digit years from year to limit * @param {Number} - a 2-digit representation of the year * @param {Number} - an upper bound for generating our list * @returns {[Number]} - a list of 2-digit years */ // Is this pure? Why/why not? const yearsFrom = (year, limit) => [year] // first, throw the year into an Array .map(y => range(limit) // now, we can map() and reduce()! .map(n => sum(n)(year)))// sum each element in our range to our year and collect .reduce(xs => xs); // reduce on the identity function to return our list of years // convenience function to generate a list of years limit years from now // Is this pure? Why/why not? const yearsFromNow = limit => yearsFrom(thisYear(), limit); // What about this? Is this okay? const yearsFromNow2 = yearsFrom(thisYear(), 15); // And this? const yearsFromNow3 = yearsFrom(thisYear2, 15);
Note: If we give either of these functions the wrong type, they'll blow up! That's fine for now! We'll be revisiting that issue when we build on the concept of a collection pipeline in our next talk.
const fifteenYearsFromNow = yearsFromNow(15);
Note from CJ: "...impurity spreads like contagion...in most code—where the pure & impure parts are mixed—the impure parts contaminate the potentially pure parts (all the way up the call stack!), because side-effects are contagious." Note from Carlo: I would add that the impact of this impurity spreading throughout a codebase is very serious. It destabilizes the system over time and eats up engineer hours in debugging, refactoring, maintenance and support.
Final thought/homework: What if we took this pattern of throwing a thing into a collection and popping it back out again and turned that into a Javascript object that we can reuse anywhere? What would we have then? What would it look like? What might we be able to do? How might this help with the problem of types, especially null and undefined? How might we refactor these functions to use this new kind of object? Can you find out what you call a collection type that can store data and provides higher-order functions like map() and reduce()?
Thank you! 🤓
Loading…

no comments

    sign in to comment