{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# JavaScript pragmatics: functional style\n", "\n", "## Headnotes\n", "\n", "* Procedural vs. functional \"**paradigms**\", imperative vs. declarative **style**\n", "* Good interactive tutorial: " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Considering different styles for solving problems\n", "\n", "* **Example:** Is order of array items important, or do you need to use the array length property? If not, use `for (element in array)` (imperative). If so, use a `while` or `for` loop (imperative) or `Array.prototype.forEach()` (declarative). To transform elements of an array, use `Array.prototype.map()`; to filter an array, use `Array.prototype.filter()`; to resolve or concatenate values in an array, use `Array.prototype.reduce()`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Chaining method calls" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If you use meaningful callback function parameter names and indentation, these method calls are largely self-documenting even when chained:" ] }, { "cell_type": "code", "execution_count": 36, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "igpay atinlay\n" ] }, { "data": { "text/plain": [ "undefined" ] }, "execution_count": 36, "metadata": {}, "output_type": "execute_result" } ], "source": [ "function pigLatin(str) {\n", " return str.toLowerCase().split(' ')\n", " .map(function(word) {\n", " return word.substring(1) +\n", " word.charAt(0) + 'ay';\n", " }).join(' ');\n", "}\n", "\n", "console.log(pigLatin('Pig latin')); // => igPay atinlay" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Returning a function" ] }, { "cell_type": "code", "execution_count": 40, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "fooblue\n", "footrue\n" ] }, { "data": { "text/plain": [ "undefined" ] }, "execution_count": 40, "metadata": {}, "output_type": "execute_result" } ], "source": [ "function makeStrConcatenator(str) {\n", " return function(val) {\n", " return str + val;\n", " }\n", "}\n", "\n", "var fooConcatenator = makeStrConcatenator('foo');\n", "\n", "console.log(fooConcatenator('blue')); // => 'fooblue'\n", "console.log(fooConcatenator(true)); // => 'footrue'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Examples\n", "\n", "### Testing code used in some examples" ] }, { "cell_type": "code", "execution_count": 19, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "undefined" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "function terseTest(exp) {\n", " return exp\n", " ? 'success '\n", " : 'FAILURE '\n", "}\n", "\n", "// Expects an array of objects resembling {input: 'foo', output: 'foobar'}\n", "function testResults(testFunction, reporter, tests) {\n", " var keys, results = '';\n", " tests.forEach(function (test) {\n", " keys = Object.keys(test);\n", " results += reporter(testFunction(test[keys[0]]) === test[keys[1]]);\n", " });\n", " return results;\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Example of progressively more \"functional\" approaches to a simple task:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[ 'foo 3', 'bar 3' ]\n" ] }, { "data": { "text/plain": [ "undefined" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "// Imperative solution using iteration, ES1 for..in\n", "function addLength(str) {\n", " var arr = str.split(' '), elt, word;\n", " for (elt in arr) {\n", " word = arr[elt];\n", " arr[elt] = word + ' ' + word.length;\n", " }\n", " return arr;\n", "}\n", "\n", "// Imperative solution using iteration, ES5 forEach()\n", "function addLength(str) {\n", " var arr = str.split(' ');\n", " arr.forEach(function(val, ind, arr) {\n", " arr[ind] = val + ' ' + val.length;\n", " });\n", " return arr;\n", "}\n", "\n", "// Declarative solution, ES5 map()\n", "function addLength(str) {\n", " return str.split(' ').map(function(val) {\n", " return val + ' ' + val.length;\n", " });\n", "}\n", "\n", "// Declarative solution, ES5 map(), ES6 arrow function\n", "// function addLength(str) {\n", "// return str.split(' ').map(val => val + ' ' + val.length);\n", "// }\n", "\n", "console.log(addLength('foo bar')); // => [\"foo 3\", \"bar 3\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Take a sparse array, return a dense array\n", "\n", "#### Imperative solution\n", "\n", "Loop and transfer values to a new array. I couldn't find a working solution using `Array.prototype.splice()`, which would be another option." ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[ 1, 2, 3, 4, 5, 10 ]\n" ] }, { "data": { "text/plain": [ "undefined" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "function returnSparse(arr) {\n", " var dense = [];\n", " for (var i = 0, len = arr.length; i < len; i++) {\n", " if (arr[i] !== null && arr[i] !== undefined)\n", " dense.push(arr[i]);\n", " }\n", " return dense;\n", "}\n", "\n", "var sparse = [1,,null,,2,,null,undefined,,3,,4,5,,,10];\n", "console.log(returnSparse(sparse)); // => [1, 2, 3, 4, 5, 10];" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Declarative solutions" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Using `Array.prototype.filter()`:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[ 1, 2, 3, 4, 5, 10 ]\n" ] }, { "data": { "text/plain": [ "undefined" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "function returnSparse(arr) {\n", " return arr.filter(function(val) {\n", " return val !== null && val !== undefined;\n", " });\n", "}\n", "\n", "var sparse = [1,,null,,2,,null,undefined,,3,,4,5,,,10];\n", "console.log(returnSparse(sparse)); // => [1, 2, 3, 4, 5, 10];" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "With ES6 arrow function:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "// TODO: add output once Node supports arrow functions\n", "function returnSparse(arr) {\n", " return arr.filter(val =>\n", " val !== null && val !== undefined\n", " );\n", "}\n", "\n", "var sparse = [1,,null,,2,,null,undefined,,3,,4,5,,,10];\n", "console.log(returnSparse(sparse)); // => [1, 2, 3, 4, 5, 10];" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Remove elements with falsey values from an array\n", "\n", "#### Imperative solutions" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "// Imperative solution using iteration.\n", "function removeFalsey(arr) {\n", " var out = [];\n", " for (var elt in arr) if (arr[elt]) out.push(arr[elt]);\n", " return out;\n", "}\n", "\n", "// Imperative solution using recursion.\n", "function removeFalsey(arr) {\n", " while (arr.some(function(elt) { return (!elt) })) {\n", " var first = arr.shift();\n", " if (!!first) arr.push(first);\n", " removeFalsey(arr);\n", " }\n", " return arr;\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Declarative solutions" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "// Declarative solution.\n", "function removeFalsey(arr) {\n", " return arr.filter(function(elt) {\n", " return (!!elt);\n", " });\n", "}\n", "\n", "// Using ES6 arrow function.\n", "function removeFalsey(arr) {\n", " return arr.filter(elt => (!!elt));\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Transform a string" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Insert dashes between odd numbers in num, ignoring zero\n", "\n", "Imperative solution, using a new string:" ] }, { "cell_type": "code", "execution_count": 29, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "success success success \n" ] }, { "data": { "text/plain": [ "undefined" ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ "function insertDash(num) {\n", " var dashed = ''; num = num.toString();\n", " for (var i = 0, len = num.length; i < len; i++) {\n", " dashed += num[i];\n", " if (num[i] !== '0' && // Don't treat zero as odd\n", " i !== len - 1 && // Don't append dash to last numeral\n", " Number(num[i]) % 2 !== 0 &&\n", " Number(num[i + 1]) % 2 !== 0) dashed += '-';\n", " }\n", " return dashed;\n", "}\n", "\n", "var testInsertDash = function() {\n", " var testFunction = insertDash,\n", " reporter = terseTest,\n", " tests = [\n", " {i: 333565, o: '3-3-3-565'},\n", " {i: 454703, o: '454703'},\n", " {i: 454793, o: '4547-9-3'}\n", " ];\n", "\n", " console.log(testResults(testFunction, reporter, tests));\n", "}\n", "testInsertDash();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Recursive solution with regexp:" ] }, { "cell_type": "code", "execution_count": 31, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "success success success \n" ] }, { "data": { "text/plain": [ "undefined" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "function insertDash(num) {\n", " var re = /([13579](?=[13579]))/;\n", " num = num.toString();\n", " while (re.test(num)) {\n", " num = num.replace(re, \"$1-\");\n", " insertDash(num);\n", " }\n", " return num;\n", "}\n", "\n", "testInsertDash();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Declarative solution, using `Array.prototype.reduce`. The first value passed to the callback will be the entire concatenated string in each case, so we need a regexp that finds odd numbers only *at the end* of the expression:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[3] [3]\n", "[3-3] [3]\n", "[3-3-3] [5]\n", "[3-3-3-5] [6]\n", "[3-3-3-56] [5]\n" ] }, { "data": { "text/plain": [ "'3-3-3-565'" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "'333565'.split('').reduce(function(a, b) {\n", " console.log('[' + a + ']', '[' + b + ']');\n", " return (/[13579]$/.test(a) && /[13579]$/.test(b))\n", " ? (a + '-' + b)\n", " : (a + b);\n", "});" ] }, { "cell_type": "code", "execution_count": 32, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "success success success \n" ] }, { "data": { "text/plain": [ "undefined" ] }, "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "function insertDash(num) {\n", " var arr = num.toString().split(''),\n", " re = /[13579]$/;\n", " return arr.reduce(function (a, b) {\n", " return (re.test(a) && re.test(b))\n", " ? (a + '-' + b)\n", " : (a + b);\n", " });\n", "}\n", "\n", "testInsertDash();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Declarative solution, ES6 arrow function:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "function insertDash(num) {\n", " var arr = num.toString().split(''),\n", " re = /[13579]$/;\n", " return arr.reduce((a, b) =>\n", " (re.test(a) && re.test(b))\n", " ? (a + '-' + b)\n", " : (a + b)\n", " );\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### PascalCase to snake_case\n", "\n", "Treating lowercase characters as numbers." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Imperative solution:" ] }, { "cell_type": "code", "execution_count": 35, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "success success success \n" ] }, { "data": { "text/plain": [ "undefined" ] }, "execution_count": 35, "metadata": {}, "output_type": "execute_result" } ], "source": [ "function camelToSnake(string) {\n", " var re = /([A-Z]{1}[a-z0-9]*)/g,\n", " words = string.match(re),\n", " snaked = '';\n", " for (var i = 0, len = words.length; i < len; i++) {\n", " snaked += (i != len - 1) ? (words[i] + '_') : words[i];\n", " }\n", " return snaked.toLowerCase();\n", "}\n", "\n", "var testCamelToSnake = function() {\n", " var testFunction = camelToSnake,\n", " reporter = terseTest,\n", " tests = [\n", " {i: 'TestController', o: 'test_controller'},\n", " {i: 'MoviesAndBooks', o: 'movies_and_books'},\n", " {i: 'App7Test', o: 'app7_test'}\n", " ];\n", "\n", " console.log(testResults(testFunction, reporter, tests));\n", "}\n", "\n", "testCamelToSnake();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Declarative solution using `String.prototype.match()`:" ] }, { "cell_type": "code", "execution_count": 37, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "success success success \n" ] }, { "data": { "text/plain": [ "undefined" ] }, "execution_count": 37, "metadata": {}, "output_type": "execute_result" } ], "source": [ "function camelToSnake(string) {\n", " var re = /([A-Z]{1}[a-z0-9]*)/g;\n", " return string.match(re).join('_').toLowerCase();\n", "}\n", "\n", "testCamelToSnake();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Declarative solution using `String.prototype.split()` and a regexp with lookahead assertion. If a regexp contains grouping parentheses, the `split()` method splices the portion of the match between grouping parens into the output array." ] }, { "cell_type": "code", "execution_count": 39, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "success success success \n" ] }, { "data": { "text/plain": [ "undefined" ] }, "execution_count": 39, "metadata": {}, "output_type": "execute_result" } ], "source": [ "function camelToSnake(string) {\n", " var re = /(?=[A-Z])/; // Split on anything followed by [A-Z]\n", " return string.split(re).join('_').toLowerCase();\n", "}\n", "\n", "testCamelToSnake();" ] } ], "metadata": { "kernelspec": { "display_name": "Javascript (Node.js)", "language": "javascript", "name": "javascript" }, "language_info": { "file_extension": "js", "mimetype": "application/javascript", "name": "javascript", "version": "0.12.6" } }, "nbformat": 4, "nbformat_minor": 0 }