Making Arrays Stuff More Functional
We previously covered 2 "classes" of Array methods
- Mutator Methods - Those that change the structure of an Array
- Accessor Methods - Those that access contents of the Array without changing it
We have one left...
Iterator Methods
These methods iterate over the array
forEach(callback[, arg])
Calls a function you provide for each element in the Array
function logTheThings(element, index, array){
console.log('Current element is: ', element);
}
var myArray = [3, 'taco', false, 'Wonka'];
myArray.forEach(logTheThings);
//Current element is: 3
//Current element is: 'taco'
//Current element is: false
//Current element is: 'Wonka'
Your callback function takes 3 params:
- element - the current element being iterated over
- index - the current index of the element in the Array
- array - the array itself being traversed
var myArray = [3, 'taco', false, 'Wonka'];
myArray.forEach(function(element, index, array) {
console.log('Current element is: ', element);
});
//Current element is: 3
//Current element is: 'taco'
//Current element is: false
//Current element is: 'Wonka'
=>
TIP: If you don't want the function to run against an element return false
every(callback[, arg])
Returns true if every element in the Array passes the test you provide
Your callback function takes 3 params:
- element - the current element being iterated over
- index - the current index of the element in the Array
- array - the array itself being traversed
function isNumeric(element, index, array) {
return !isNaN(parseInt(element));
}
var myArray = [3, 'taco', false, 'Wonka'];
myArray.every(isNumeric); //return false
var myOtherArray = [3, 5, 6, 9];
myArray.every(isNumeric); //return true
some(callback[, arg])
Returns true if ANY element in the Array passes the test you provide
Your callback function takes 3 params:
- element - the current element being iterated over
- index - the current index of the element in the Array
- array - the array itself being traversed
function hazNumber(element, index, array) {
return !isNaN(parseInt(element));
}
var myArray = [3, 'taco', false, 'Wonka'];
myArray.some(hazNumber); //return true, 1 iteration
var myOtherArray = ['test', 'taco', 'hotdog', {}];
myOtherArray.some(hazNumber); //return false, 4 iterations
Returns immediately when first true is found
filter(callback[, arg])
Creates and returns a new array with all elements that pass true test
Your callback function takes 3 params:
- element - the current element being iterated over
- index - the current index of the element in the Array
- array - the array itself being traversed
function getNumberz(element, index, array) {
return !isNaN(parseInt(element));
}
var myArray = [3, 'taco', false, 44, 'Wonka'];
myArray.filter(getNumberz); //return [3, 44]
console.log(myArray); //[3, 'taco', false, 44, 'Wonka']
var myOtherArray = ['test', 'taco', 'hotdog', {}];
myOtherArray.filter(getNumberz); //return []
console.log(myOtherArray); //['test', 'taco', 'hotdog', {}]
NOTE: filter() does NOT mutate the original array
map(callback[, arg])
Creates/returns a new array with all returned values from callback function
Your callback function takes 3 params:
- element - the current element being iterated over
- index - the current index of the element in the Array
- array - the array itself being traversed
function tripleIt(element, index, array) {
return element * 3;
}
var myArray = [3, 12, 1, 9];
myArray.map(tripleIt); //return [9, 36, 3, 27]
console.log(myArray) //[3, 12, 1, 9]
var myOtherArray = [3, 12, 1, 'taco'];
myOtherArray.map(tripleIt); //return [9, 36, 3, NaN]
console.log(myArray) //[3, 12, 1, 'taco']
NOTE: map() does NOT mutate the original array
reduce(callback[, initalValue])
Returns a single value accumulated against all values from callback function
Your callback function takes 4 params and 1 optional param:
- previousValue - the previous element being iterated over
- currentValue - the current element being iterated over
- index - the current index of the array
- array - the array itself being traversed
function summarize(previousValue, currentValue, index, array) {
return previousValue + currentValue;
}
var myArray = [3, 12, 1, 9];
myArray.reduce(summarize); //return 25
console.log(myArray) //[3, 12, 1, 9]
//Start at a certain initial value
myArray.reduce(summarize, 10); //return 35
console.log(myArray) //[3, 12, 1, 9]
NOTE: reduce() does NOT mutate the original array
If you pass NO optional param, first iteration is at index 1 so that previous/next have values
//Omit index and array is still valid
function summarize(previousValue, currentValue) {
return previousValue + currentValue;
}
var myArray = [3, 12, 1, 9];
myArray.reduce(summarize); //return 25
console.log(myArray) //[3, 12, 1, 9]
//Start at a certain initial value
myArray.reduce(summarize, 10); //return 35
console.log(myArray) //[3, 12, 1, 9]
reduceRight(callback[, initalValue])
Returns a single value accumulated against all values from callback function
Your callback function takes 4 params and 1 optional param:
- previousValue - the previous element being iterated over
- currentValue - the current element being iterated over
- index - the current index of the array
- array - the array itself being traversed
NOTE: reduceRight() does NOT mutate the original array
reduceRight() has the same functionality as reduce() except that it starts from the right side of the input array
References
Javascript is a
multi-paradigm
language
- Imperative
- Functional
Imperative/ Procedural
How does it work?
Defines computation as statements that change a program state.
Functional/ Declarative
What does it do?
Express the desired result by describing rules, constraints, and relationships-- without explicitly mutating state.
Before computers were invented, mathematicians invented two
Universal Models of Computation
(1) Turing Machines
(state visualized as a ticker tape of infinite length)
(2) Lambda Calculus
(no state, just functions)
Turing eventually proved both Universal Models of Computation are mathematically equivalent - both can express any computable algorithm
As it turned out, Turing Machines very closely matched the actual hardware of computers. Instead of infinite ticker tape, real computers have finite memory, but everything else is the same.
Imperative programming became by far the dominant paradigm.
Lambda calculus was majorly sidelined for decades... until...
Intro to Lambda Calculus
Javascript!
brought functional programming to the masses
JS was the first popular programming language combining the power of "higher order functions" with C-like syntax most programmers are familiar with.
"Higher order functions" means functions can take functions as parameters and can return functions as output.
Functions are values
This is a function that returns a function
const greaterThan = a => b => b > a
// use it like this:
const greaterThan10 = greaterThan(10)
console.log(greaterThan10(11)) // true
This is a function that takes
a function as a parameter
const maybeDouble = (predicate, n) =>
predicate(n) ? n * 2 : n
// use it like this:
const isOdd = n => n % 2 === 1
console.log(maybeDouble(isOdd, 5)) // 10
btw, "predicate" is just a fancy word for a function that returns only true or false. There are lots of fancy words in functional programming for things that are actually quite simple. Don't be intimidated.
Composition
Suppose we have two functions:
const addTwo = n => n + 2
const multiplyByFour = n => 4 * n
Note that there are two ways to compose them:
const multiplyByFourThenAddTwo =
x => addTwo(multiplyByFour(x))
const addTwoThenMultiplyByFour =
x => multiplyByFour(addTwo(x))
Text
Text
The cornerstone of functional programming is...
Problem
Manually invoking functions in series is tedious, error-prone, and difficult for code re-use.
Consider:
func1(func2(func3(func4(func5(func6(func7(x)))))))
Can we do better?
const a = func7(x)
const b = func6(a)
const c = func5(b)
const d = func4(c)
const e = func3(d)
const f = func2(e)
const g = func1(f)
Or:
const funcs = [addTwo, multipleByFour]
Imagine an array of functions...
How can we compose them in the general case?
const compose = (...funcs) => input =>
funcs.reduceRight((prevOutput, nextFunc) =>
nextFunc(prevOutput), input)
const pipe = (...funcs) => input =>
funcs.reduce((prevOutput, nextFunc) =>
nextFunc(prevOutput), input)
compose(...funcs)(4) // 18, right-to-left
pipe( ...funcs)(4) // 24, left-to-right
// imagine beging able to rewrite this:
const myNumber = Math.round(Math.sqrt(Math.PI))
// as this:
const myNumber = Math.PI |> Math.sqrt |> Math.round
// other symbols considered for the pipeline operator:
// :: -> ~> ..
Sidenote: did you know future javascript might have a pipeline operator?
Test it out here
Most programmers prefer pipe over compose
Although some mathematicians prefer compose
(it's sort of like reverse polish notation),
pipe is like English
(and many other languages):
data flows left-to-right, top-to-bottom.
Notice something about the functions you pass to compose and pipe?
They all have exactly one parameter.
In functional programming, we say they all have an arity of one (unary).
Arity is just a fancy word for the number of parameters a function has.
In pure functional programming
(lambda calculus), all functions have an arity of exactly one.
Currying
The next concept you are likely to run into in functional programming is
Named after Haskell Curry, mathematician
Currying is the process of converting a function, that accepts n arguments, into a sequence of n chained function calls, having exactly one argument each.
// uncurried
const multiply = (a, b) => a * b
multiply(2, 3) // 6
// curried
const curriedMultiply = a => b => a * b
curriedMultiply(2)(3) // 6
Can we curry automagically?
Glad you asked...
const curry = (f, arr = []) => (...args) => (
a => a.length === f.length
? f(...a)
: curry(f, a)
)([...arr, ...args])
// currying gives us SUPERPOWERS, like this:
const multiply = (a, b) => a * b
const multiplyBy4 = curry(multiply)(4)
multiplyBy4(3) // 12
// now we can convert any multi-arity function
// into a Function Factory :)
Going back to our original example
// remember this?
// const funcs = [addTwo, multipleByFour]
// now we can refactor it like this:
const add = (a, b) => a + b
const multiply = (a, b) => a * b
const addTo = curry(add)
const multiplyBy = curry(multiply)
const funcs = [addTo(2), multiplyBy(4)]
pipe(...funcs)(4) // 24
But wait there's more...
So how do you choose which functional library to use?
Lodash is by far the most popular library, but lodash/fp is comparatively little-known. lodash/fp functions are wrappers around the better known non-fp lodash functions, so if you are already using lodash, lodash/fp is probably your best choice.
Ramda is a little more hardcore functional than lodash/fp because most of the functions assume currying. If you are a functional geek, you might enjoy ramda over lodash/fp.
Rambda bills itself as smaller and faster than ramda, but it is probably the most obscure choice, and has the fewest utility functions.
Making Stuff More Functional
By Jason Sewell
Making Stuff More Functional
- 1,413