Alonzo Church • Church-Turing Thesis • Lambda Calculus • Represent computation in terms of “function abstraction and application using variable binding and substitution.”
Represent computation in terms of “function abstraction and application using variable binding and substitution.” λ-Calculus Bind “x” to function => parameters
Represent computation in terms of “function abstraction and application using variable binding and substitution.” λ-Calculus Substitution => apply arguments
– Wikipedia “In computer science, functional programming is a programming paradigm — a style of building the structure and elements of computer programs — that treats computation as the evaluation of mathematical functions and avoids changing- state and mutable data. It is a declarative programming paradigm, which means programming is done with expressions.” What is Functional Programming?
Imperative • Most familiar style of programming • Telling the computer how to accomplish a task • Algorithms are a series of steps • Mutable data usually involved • C, C++, Java
Declarative • Typically associated with functional programming • Telling the computer what you want, not how to get it • Algorithms are compositions of functions • Immutable data (or minimal side effects) preferred • SQL, HTML, Haskell, DSLs
First Class Functions Functions are expressions/values. // Function declaration function add(x, y) { return x + y; } // Assign to a variable const addAlias = add; // Assign an anonymous function expression const multiply = (x, y) => x * y; // Functions have properties console.log(add.name); // 'add' console.log(addAlias.name); // 'add' console.log(multiply.name); // ''
First Class Functions Functions are expressions/values. // Function declaration function add(x, y) { return x + y; } // Assign to a variable const addAlias = add; // Assign an anonymous function expression const multiply = (x, y) => x * y; // Functions have properties console.log(add.name); // 'add' console.log(addAlias.name); // 'add' console.log(multiply.name); // ''
First Class Functions Functions are expressions/values. // Function declaration function add(x, y) { return x + y; } // Assign to a variable const addAlias = add; // Assign an anonymous function expression const multiply = (x, y) => x * y; // Functions have properties console.log(add.name); // 'add' console.log(addAlias.name); // 'add' console.log(multiply.name); // ''
First Class Functions Functions are expressions/values. // Function declaration function add(x, y) { return x + y; } // Assign to a variable const addAlias = add; // Assign an anonymous function expression const multiply = (x, y) => x * y; // Functions have properties console.log(add.name); // 'add' console.log(addAlias.name); // 'add' console.log(multiply.name); // ''
First Class Functions Functions as arguments // Passing in anonymous functions myEventLib.on('update', (data) => console.log(data)); function doubleNumber(n) { return n * 2; } // Earlier example, passing in named function function doubleNumbers(numbers) { return numbers.map(doubleNumber); }
First Class Functions Functions as arguments // Passing in anonymous functions myEventLib.on('update', (data) => console.log(data)); function doubleNumber(n) { return n * 2; } // Earlier example, passing in named function function doubleNumbers(numbers) { return numbers.map(doubleNumber); }
First Class Functions Functions as return values // Return a new function that adds a number // to another function additionFactory(x) { return (y) => x + y; } const add1 = additionFactory(1); add1(2); // 3
First Class Functions Functions as return values // Return a new function that adds a number // to another function additionFactory(x) { return (y) => x + y; } const add1 = additionFactory(1); add1(2); // 3
First Class Functions Functions as return values // Return a new function that adds a number // to another function additionFactory(x) { return (y) => x + y; } const add1 = additionFactory(1); add1(2); // 3
Closures • Allow functions to reference other scopes • “Close over” variables in higher scopes • Critical to functional programming paradigms like partial application
Referential Transparency • Fuzzy term associated with purity • For a given input, a function always returns the same value • Replace function calls with return value • No dependency on state outside function
Referential Transparency function doubleNumber(n) { return n * 2; } function square(n) { return n * n; } function add6(n) { return n + 6; } // Reduces down to the same value let value1 = add6(square(doubleNumber(3))); // 42 let value2 = add6(square(6)); // 42 let value3 = add6(36); // 42 let value4 = 42; // Same value for every call let otherValue1 = doubleNumber(3) + doubleNumber(3) + doubleNumber(3) + doubleNumber(3); let otherValue2 = 6 + 6 + 6 + 6;
Referential Transparency function doubleNumber(n) { return n * 2; } function square(n) { return n * n; } function add6(n) { return n + 6; } // Reduces down to the same value let value1 = add6(square(doubleNumber(3))); // 42 let value2 = add6(square(6)); // 42 let value3 = add6(36); // 42 let value4 = 42; // Same value for every call let otherValue1 = doubleNumber(3) + doubleNumber(3) + doubleNumber(3) + doubleNumber(3); let otherValue2 = 6 + 6 + 6 + 6;
Referential Transparency function doubleNumber(n) { return n * 2; } function square(n) { return n * n; } function add6(n) { return n + 6; } // Reduces down to the same value let value1 = add6(square(doubleNumber(3))); // 42 let value2 = add6(square(6)); // 42 let value3 = add6(36); // 42 let value4 = 42; // Same value for every call let otherValue1 = doubleNumber(3) + doubleNumber(3) + doubleNumber(3) + doubleNumber(3); let otherValue2 = 6 + 6 + 6 + 6;
Referential Transparency let counter = 0; function addToCounter1(n) { counter++; return n + counter; } function addToCounter2(n) { let counter = 0; counter++; return n + counter; } // Not referentially transparent let value1 = addToCounter1(1) + addToCounter1(1); // 5 let value2 = 2 + 3; // Not the same value every call! // Referentially transparent let otherValue1 = addToCounter2(1) + addToCounter2(1); // 4 let otherValue2 = 2 + 2;
Referential Transparency let counter = 0; function addToCounter1(n) { counter++; return n + counter; } function addToCounter2(n) { let counter = 0; counter++; return n + counter; } // Not referentially transparent let value1 = addToCounter1(1) + addToCounter1(1); // 5 let value2 = 2 + 3; // Not the same value every call! // Referentially transparent let otherValue1 = addToCounter2(1) + addToCounter2(1); // 4 let otherValue2 = 2 + 2;
Referential Transparency let counter = 0; function addToCounter1(n) { counter++; return n + counter; } function addToCounter2(n) { let counter = 0; counter++; return n + counter; } // Not referentially transparent let value1 = addToCounter1(1) + addToCounter1(1); // 5 let value2 = 2 + 3; // Not the same value every call! // Referentially transparent let otherValue1 = addToCounter2(1) + addToCounter2(1); // 4 let otherValue2 = 2 + 2;
Pure Functions function add(x, y) { return x + y; } function capitalize(string) { return string[0].toUpperCase() + string.slice(1).toLowerCase(); } function toTitleCase(string) { return string .split(/\s+/) .map(capitalize) .join(' '); } • No side effects. • String and number arguments are immutable.
Recursion • Defining a solution to a problem in terms of itself • Solve the problem by solving smaller pieces • Similar to inductive proofs • In programming, a function that calls itself • In FP, imperative looping (e.g. with while or for) is practically nonexistent, so solve with recursion
Tail Call Optimization • Optimize recursive functions to not exhaust max stack size • Replace stack frames instead of adding new ones • The last statement before returning must be recursive call • Typically rewrite function to include an accumulator that holds the current result • (Coming to JavaScript in ES2015!)
Optimize Factorial function factorial(n, accum = 1) { if (n < 2) { return accum; } return factorial(n - 1, n * accum); } Return the accumulator for base case
Performance Revisited fibonacci(1); // 0 ms fibonacci(5); // 0 ms fibonacci(20); // 0 ms fibonacci(30); // 13 ms fibonacci(35); // 131 ms fibonacci(40); // 1516 ms
Performance Revisited fibonacci(1); // 0 ms fibonacci(5); // 0 ms fibonacci(20); // 0 ms fibonacci(30); // 13 ms fibonacci(35); // 131 ms fibonacci(40); // 1516 ms fibonacci(45); // 16.6 seconds!
Performance Revisited fibonacci(1); // 0 ms fibonacci(5); // 0 ms fibonacci(20); // 0 ms fibonacci(30); // 13 ms fibonacci(35); // 131 ms fibonacci(40); // 1516 ms fibonacci(45); // 16.6 seconds! fibonacci(100); // Who knows how long
TCO and Dynamic Programming • Build up the result from the leaves of the tree • Avoids recalculating the same values • Make recursive calls in the reverse direction
Fibonacci TCO function fibonacci(n, current = 0, next = 1) { if (n === 0) { return current; } return fibonacci(n - 1, next, current + next); } Two accumulators
Fibonacci TCO function fibonacci(n, current = 0, next = 1) { if (n === 0) { return current; } return fibonacci(n - 1, next, current + next); } Results for next call
Fibonacci TCO function fibonacci(n, current = 0, next = 1) { if (n === 0) { return current; } return fibonacci(n - 1, next, current + next); } Reach the end, so return whatever was built up
Fibonacci Performance fibonacci(1); // 0 ms fibonacci(5); // 0 ms fibonacci(20); // 0 ms fibonacci(30); // 0 ms fibonacci(35); // 0 ms fibonacci(40); // 0 ms fibonacci(45); // 0 ms fibonacci(100); // 0 ms
Array#map Map values in array to other values in new array function map(fn, array) { if (array.length === 0) { return array; } const [head, ...tail] = array; return [fn(head), ...map(fn, tail)]; }
Array#map Map values in array to other values in new array function map(fn, array) { if (array.length === 0) { return array; } const [head, ...tail] = array; return [fn(head), ...map(fn, tail)]; } Function first; good for currying
Array#map Map values in array to other values in new array function map(fn, array) { if (array.length === 0) { return array; } const [head, ...tail] = array; return [fn(head), ...map(fn, tail)]; } Grab the first element and remaining elements
function map(fn, array) { if (array.length === 0) { return array; } const [head, ...tail] = array; return [fn(head), ...map(fn, tail)]; } Array#map Map values in array to other values in new array var head = array[0];
function map(fn, array) { if (array.length === 0) { return array; } const [head, ...tail] = array; return [fn(head), ...map(fn, tail)]; } Array#map Map values in array to other values in new array var tail = array.slice(1);
Array#map Map values in array to other values in new array function map(fn, array) { if (array.length === 0) { return array; } const [head, ...tail] = array; return [fn(head), ...map(fn, tail)]; } Return new array with altered head and mapped array of remaining elements
Array#map Map values in array to other values in new array function map(fn, array) { if (array.length === 0) { return array; } const [head, ...tail] = array; return [fn(head), ...map(fn, tail)]; } Base case: empty array.
Array#map Map values in array to other values in new array function square(n) { return n * n; } let numbers = [1, 2, 3]; let squaredNumbers = map(square, numbers); // [1, 4, 9];
Partial Application • Assign values to function parameters without evaluating the function (i.e. “prefill” argument values) • Produce a new function that takes in any remaining unassigned arguments
Partial Application // Before we would use this function additionFactory(x) { return (y) => x + y; } let add1 = additionFactory(1); add1(2); // 3 // Now we can write an add function // and use partial application function add(x, y) { return x + y; } // Partial application with native `bind` function add1 = add.bind(null, 1); add1(2); // 3 // Use a custom-written `partial` function add1 = partial(add, 1); add1(2); // 3
Partial Application // Before we would use this function additionFactory(x) { return (y) => x + y; } let add1 = additionFactory(1); add1(2); // 3 // Now we can write an add function // and use partial application function add(x, y) { return x + y; } // Partial application with native `bind` function add1 = add.bind(null, 1); add1(2); // 3 // Use a custom-written `partial` function add1 = partial(add, 1); add1(2); // 3
Partial Application // Before we would use this function additionFactory(x) { return (y) => x + y; } let add1 = additionFactory(1); add1(2); // 3 // Now we can write an add function // and use partial application function add(x, y) { return x + y; } // Partial application with native `bind` function add1 = add.bind(null, 1); add1(2); // 3 // Use a custom-written `partial` function add1 = partial(add, 1); add1(2); // 3
Partial Application // Before we would use this function additionFactory(x) { return (y) => x + y; } let add1 = additionFactory(1); add1(2); // 3 // Now we can write an add function // and use partial application function add(x, y) { return x + y; } // Partial application with native `bind` function add1 = add.bind(null, 1); add1(2); // 3 // Use a custom-written `partial` function add1 = partial(add, 1); add1(2); // 3
Partial Application function partial(fn, ...args) { return (...otherArgs) => { return fn(...args, ...otherArgs); }; } Call original function with all arguments
Partial Application: Why? • Build up functions from smaller pieces • Remove duplication • Generalize function abstraction (i.e. no additionFactory type functions)
Currying • Similar to partial application • Bakes partial application into a function • Successive invocations of function with arguments returns a new function with those arguments assigned • Keep returning a new function until all arguments “filled,” and then invoke the actual function with all of those arguments (sounds recursive, huh?)
Currying const add = curry((x, y, z) => x + y + z); // All arguments supplied, normal invocation. add(1, 2, 3); // 6 // Supply arguments one at a time. Final argument // induces invocation. add(1)(2)(3); // 6 // Equivalent to previous example. let add1 = add(1); let add3 = add1(2); add3(3); // 6 // Supply more than one argument at a time. add(1, 2)(3); // 6 add(1)(2, 3); // 6
Currying const add = curry((x, y, z) => x + y + z); // All arguments supplied, normal invocation. add(1, 2, 3); // 6 // Supply arguments one at a time. Final argument // induces invocation. add(1)(2)(3); // 6 // Equivalent to previous example. let add1 = add(1); let add3 = add1(2); add3(3); // 6 // Supply more than one argument at a time. add(1, 2)(3); // 6 add(1)(2, 3); // 6
Currying const add = curry((x, y, z) => x + y + z); // All arguments supplied, normal invocation. add(1, 2, 3); // 6 // Supply arguments one at a time. Final argument // induces invocation. add(1)(2)(3); // 6 // Equivalent to previous example. let add1 = add(1); let add3 = add1(2); add3(3); // 6 // Supply more than one argument at a time. add(1, 2)(3); // 6 add(1)(2, 3); // 6
Currying const add = curry((x, y, z) => x + y + z); // All arguments supplied, normal invocation. add(1, 2, 3); // 6 // Supply arguments one at a time. Final argument // induces invocation. add(1)(2)(3); // 6 // Equivalent to previous example. let add1 = add(1); let add3 = add1(2); add3(3); // 6 // Supply more than one argument at a time. add(1, 2)(3); // 6 add(1)(2, 3); // 6
Currying const add = curry((x, y, z) => x + y + z); // All arguments supplied, normal invocation. add(1, 2, 3); // 6 // Supply arguments one at a time. Final argument // induces invocation. add(1)(2)(3); // 6 // Equivalent to previous example. let add1 = add(1); let add3 = add1(2); add3(3); // 6 // Supply more than one argument at a time. add(1, 2)(3); // 6 add(1)(2, 3); // 6
Currying const add = curry((x, y, z) => x + y + z); // All arguments supplied, normal invocation. add(1, 2, 3); // 6 // Supply arguments one at a time. Final argument // induces invocation. add(1)(2)(3); // 6 // Equivalent to previous example. let add1 = add(1); let add3 = add1(2); add3(3); // 6 // Supply more than one argument at a time. add(1, 2)(3); // 6 add(1)(2, 3); // 6
Function Composition • Compose functions together to form new functions • Pipe function output to next function • Helps enforce modularity/ SOC by keeping functions small
Why FP • Concise, elegant solutions to problems • Modularity and composition • Eliminate data races with immutability • Unit tests are simpler — no checking for state changes