Functional Light Javascript Notes
Table of Contents
- Functional Light Javascript Notes
- Table of Contents
Function Purity
Functions versus Procedures
Functional programming is not all about the function keyword. A procedure is a collection of things we need to do in a program. Just because it uses the function keyword does not necessarily means it makes it a function. A function must take some input, but also return some output (i.e no return statement = not a function).
Naming Semantics
As discussed in the clean code textbook, we want our function names to accurately specify the true nature of the function. A function is the semantic relationship between input and computed output. If we are computing the rate of shipping an item, then obviously we would want to call our function something like 'calculateShippingRate'.
Side Effects
As discussed in the react course we want to avoid side effects wherever possible. Functions should only take an input, do something with it, then return an output. Changing global variables from within a function are considered side effects. Other side effects:
- Input/output (console.log for example)
- Database Storage
- Network Calls
- DOM
- Timestamps
- Random Numbers
Obviously we can't always avoid these, so we want to make them very obvious when used, but generally avoid wherever possible.
Pure Functions
A 'pure' function is one that takes all its inputs directly, has no side effects, and outputs everything directly. An example of pure versus impure.
// pure
function addTwo(x, y) {
return x + y
}
// impure
function addAnother(x, y) {
return addTwo(x, y) + z
}
Reducing Surface Area
function addAnother(z) {
return function addTwo(x, y) {
return x + y + z
}
}
addAnother(1)(20, 21) // 42
Function purity can be considered as a level of confidence. The more pure the function, the higher our confidence the function is working correctly, and will continue to do so.
Argument Adaptors
Function Arguments
// unary (monadic)
function increment(x) {
return sum(x, 1)
}
// binary (dyadic)
function sum(x, y) {
return x + y
}
The shape of functions is very important to think about. Kyle uses the example of two different pieces of lego. If the lego (the functions) shape do not fit, then obviously they cannot work properly together.
However we can begin to adapt the shape of a function. A higher order function, is a function which either receives (as inputs) 1 or more functions and/or returns 1 or more functions.
function unary(fn) {
// Higher order function
return function one(arg) {
return fn(arg)
}
}
function binary(fn) {
return function two(arg1, arg2) {
return fn(arg1, arg2)
}
}
var g = unary(f)
var h = binary(f)
g(1, 2, 3, 4) // [1]
h(1, 2, 3, 4) // [1, 2]
Flip, Spread and Reverse Adaptors
An example function to flip the order of arguments passed in.
function flip(fn) {
return function flipped(arg1, arg2, ...args) {
return fn(arg2, arg1, ...args)
}
}
function f(...args) {
return args
}
var g = flip(f)
g(1, 2, 3, 4) // [2, 1, 3, 4]
Similarly, the flip function could be altered to reverse the order of the arguments. Here the final return statement would be '...args.reverse();'. The rest of the setup would be the same. If we wanted to spread a single array into multiple arguments to a function, we could use the spread operator (as seen in function f), within the 'flip' function to spread arguments from one to multiple.
Point Free
Defining a function without needing to define its points (its inputs).
getPerson(function onPerson(person) {
return renderPerson(person)
})
Person, is the point for the onPerson function. Could simply be re-written as:
getPerson(renderPerson)
Another example:
function isOdd(v) {
return v % 2 == 1
}
function isEven(v) {
return !isOdd(v)
}
isEven(4)
The way of creating isEven in terms of isOdd, it highlights the relationship between the two functions, and their purpose. We do know that this could be written differently (likely all with less lines).
function not(fn) {
return function negated(...args) {
return !fn(...args)
}
}
function isOdd(v) {
return v % 2 == 1
}
var isEven = not(isOdd)
isEven(4) // true
This is another way, where the readability becomes even clearer, the second last line reads far better, and it works in a way that we don't need to see the rest of the earlier code.
Advanced Point Free
function mod(y) {
return function forX(x) {
return x % y
}
}
function eq(y) {
return function forX(x) {
return x === y
}
}
var mod2 = mod(2) // mod2 is a function which has y = 2, but requires x
var eq1 = eq(1) // eq1 is a function which has y = 1, but requires x value
function isOdd(x) {
return eq1(mod2(x))
}
This can be expanded further to a point free image:
function compose(fn2, fn1) {
return function composed(v) {
return fn2(fn1(v))
}
}
var isOdd = compose(eq1, mod2)
isOdd(3) // true
Closure
Closure is when a function 'remembers' the variables around it even when that function is executed elsewhere.
function makeCounter() {
var counter = 0
return function increment() {
return counter++
}
}
c() // 1
c() // 2
c() // 3
This is not a pure function as it returns a different output each time it is called with the same input (in this case with no arguments/input).
Memoisation
function repeater(count) {
var str
return function allTheAs() {
if (str == undefined) {
str = ''.padStart(count, 'A')
}
return str
}
}
var A = repeater(10)
A() // "AAAAAAAAAA"
A() // "AAAAAAAAAA"
What if we had another magic way where we could calculate an output from some input, then cache that information, so that future calls would return the cached object. Memoize does this for us. .padStart pads a char to a string until the required count has been met.
function repeater(count) {
return memoize(function allTheAs() {
return ''.padStart(count, 'A')
})
}
var A = repeater(10)
A() // "AAAAAAAAAA"
A() // "AAAAAAAAAA"
We can think of memoisation, as memorisation but without the r, in that they have similar concepts. It remembers values from previous iterations where the input was the same so that the output does not have to be re-calculated every time. However due to the cache nature, it will take up additional memory. So we should only use memoisation where the function will actually greatly benefit from it.
Generalised to Specialised
function ajax(url, data, cb) { /*...*/ }
ajax(CUSTOMER_API, {id:42}, renderCustomer);
function getCustomer(data, cb) {
return ajax(CUSTOMER_API, data, cb);
}
getCustomer({id:42}, renderCustomer);
function getCurrentUser(cb) {
return getCustomer({id: 42}, cb);
}
getCurrentUser(renderCustomer);
As you can see, as we move down the pairs of functions and their calls, we make a clearer and more obvious relation between them. Obviously defining this many functions will clutter up our files.
Partial Application & Currying
We can use partial application as a way of 'pre-setting' the arguments to a function.
function ajax(url, data, cb) {}
var getCustomer = partial(ajax, CUSTOMER_API)
getCustomer({ id: 42 }, renderCustomer)
A more common way is currying.
function ajax(url) {
return function getData(data) {
return function getCB(cb) {
/*...*/
}
}
}
// var ajax = url => (data => (cb => { ... }));
ajax(CUSTOMER_API)({ id: 42 })(renderCustomer)
var getCustomer = ajax(CUSTOMER_API)
var getCurrentUser = getCurrentUser({ id: 42 })
Or:
var ajax = curry(3, function ajax(url, data, cb) {
/* ... */
})
var getCustomer = ajax(CUSTOMER_API)
var getCurrentUser = getCustomer({ id: 42 })
The reasons programmers like currying, is that its a set of unary functions. They each have a single input and a single output. The style allows us to understand the code better.
The difference between partial application and currying:
- Both are specialisation techniques
- Partial application presets some arguments now, receives the rest on the next call
- Currying doesn't preset any arguments, receives each argument one at a time
Composition
Function composition is about taking the output of one function, and making it the input of another function.
function minus2(x) {
return x - 2
}
function triple(x) {
return x * 3
}
function increment(x) {
return x + 1
}
// add shipping rate
totalCost = basePrice + minus2(triple(increment(4)))
With this current setup, it's hard to reason about the different components in the statement, and we must place a lot of focus on the order of the functions. We could abstract this into another function to make this far clearer, and more reusable.
function shippingRate(x) {
return minus2(triple(increment(x)))
}
totalCost = basePrice + shippingRate(4)
We could take this even further, and create a function which itself will create our shipping rate calculation with the functions we pass to it.
function composeThree(fn1, fn2, fn3) {
return function compose(value) {
return fn1(fn2(fn3(value)))
}
}
Now we could make as many shipping rate functions as we like, perhaps we would like minus3 instead of minus2. This way makes it both point free, and far more reusable.
When we compose, we compose the functions from right-to-left as shown in the previous example. But then we could use a pipe function which operates from left-to-right. So pipeThree(fn1, fn2, fn3) would operate as fn1, fn2, fn3.
What if we try to compose non-unary functions (functions with more than 1 argument).
function sum(x, y) {
return +y
}
function triple(x) {
return x * 3
}
function divide(x, y) {
return x / y
}
divide(2, triple(sum(3, 5))) // Manual composition
// We can use currying to make the functions unary
sum = curry(2, sum)
divide = curry(2, divide)
composeThree(divBy(2), triple, sum(3))(5)
Immutability
Immutability is the idea/implication that something won't change unexpectedly. Change that needs to occur, needs to be intentional. As we know we can't re-assign a new value to a const variable directly. However we could pass it to a function, and return a mutated variable.
const shippingCost = 6.5
function increaseShipping(shipping) {
return shipping * 1.04
}
increaseShipping(shippingCost) // 6.76
The question also arises, we use const to signify a constant value in most languages. In javascript, we could assign const to an array, yet still mutate the array by changing/adding/removing values.
Object.freeze and Immutable Data Structures
Sometimes we want a read-only data-structure (we can't write to it), but it is not immutable. To do this we could use Object.freeze. However it is important to note this is shallow, so with nested objects we would have to freeze them individually too.
{
let orderDetails - {
// ...
}
processOrder(Object.freeze(orderDetails)); // Where we assume processOrder might mutate
}
Read-only data structures are those which never need to be mutated. We should make a copy, then make changes to our local copy if something needs to be mutated.
We can't treat all data-structures as read only, some will need to be mutable. When we operate on a ds, we create a new object, which behind the scenes has a pointer back to the previous version of the ds.
Immutable.js
var items = Immutable.List.of(textbook, supplies)
var updatedItems = items.push(calculator)
items === updatedItems // false
items.size // 2
updatedItems.size // 3
As you can see, updatedItems is a new data structure, which is not equal to the items ds.
Recursion
As seen in the SICP exercises, recursion can be a powerful tool for developers. Using the example of checking the number of vowels in a string, Kyle came up with the following implementation which he compared to his original for-loop implementation:
function countVowels(str) {
if (str.length == 0) return 0
var first = isVowel(str[0]) ? 1 : 0 // isVowel defined externally
return first + countVowels(str.slice(1))
}
Tail Recursion
If the last line of the function is a call to itself, then this is labelled as a tail call. It means that the recursive function will use constant space, as the current stack frame is no longer needed in the next iteration. This is also shown in our SICP exercises.
PTC or proper tail calls enables memory-efficient recursive algorithms. We first must be in strict-mode, and then out tail call must be in a proper tail-call position.
'use strict'
function decrement(x) {
return sub(x, 1)
}
function sub(x, y) {
return x - y
}
decrement(43) // 42
Obviously this is not a recursive call, simply a demonstration of decrements proper tail-call positioning. Now we can recognise that our previous countVowels example is not a proper tail-call representation.
Continuation-Passing Style
Continuation-passing is essentially a callback. Using our running example:
'use strict'
function countVowels(str, cont = (v) => v) {
// Second argument is an identity function in that is passes back what is given to it
var first = isVowel(str[0]) ? 1 : 0
if (str.length <= 1) return cont(first)
return countVowels(str.slice(1), function f(v) {
return cont(first + v)
})
}
countVowels('The quick brown fox jumps over the lazy dog')
Here we return a function, where one argument is a new function, which in turn returns the sum of first and v. This allows us to keep a running total of vowels in our string without the need of a counter. This means when we call our countVowels function, it technically has only an arity of 1, in that we need to pass it only the string for the initial call.
This does not increment the stack, but increase space on the heap.
Trampolines
A way of indicating that we want something to 'bounce' back and forth. Picture two functions calling each other back and forth, but rather than building up a stack, a function is returned which will make the next call.
function trampoline(fn) {
return function trampolined(...args) {
var result = fn(...args)
while (typeof result == 'function') {
result = result()
}
return result
}
}
This is the general setup of a trampoline function.
Data Structures Operations
A functor is a value (or collection of values), over which the values in it can be mapped.
Map: Our map transformation is required to produce a new data structure, we do not change the original. We map from one data structure to another. Map will not change the type of data structure, it will have the same shape. o
Filter: Often thought of as an exclusionary activity. But realistically, it is actually an inclusion activity, because we return true when we want to keep something (filtering into new data structure), and returning false if we don't want something (filtering out).
Reduce: Reduce is all about combining. We start with a collection of values, and some sort of initial value (typically 0 for addition and 1 for multiplication when performing operations on numbers). The initial value will be different for the use case. Our accumulator takes two inputs and accumulates them together. We can also compose nested function calls with reduce.
Transduction
Transducing is the composition of reducers. Using our previous examples with functions add1, isOdd and sum, we could manually place these functions into a reducer like so:
// Assuming add1, isOdd and sum are already defined
list.reduce(function allAtOnce(total, value) {
value = add1(value);
if(isOdd(value) {
total = sum(total, v);
} return total;)
}, 0)
This is the imperative approach. Instead if doing it like this, we would rather use transducing.
// mapReducer and filterReducer are functions provided by a functional programming library
var transducer = compose(mapReducer(add1), filterReducer(isOdd))
transduce(transducer, sum, 0, [1, 3, 4, 6, 9, 12, 13, 16, 21])
A transducer is a 'prototype' reducer, which will become a full reducer when passed a reducer. That is shown when we pass in the transducer, the sum (reducer), the initial value and the array to transduce.
What is happening 'under the hood' with these functions:
function mapWithReduce(arr, mappingFn) {
return function reducer(list, v){
list.push(mappingFn(v))
return list
};
}
function filterWithReduce(arr, predicateFn) {
return function reducer(list, v) {
if (predicateFn(v)) list.push(v)
return list
},
}
var list = [1, 3, 4, 6, 9, 12, 13, 16, 21]
list.reduce( mapReducer(add1), [] )
.reduce( filterReducer(isOdd), [] )
.reduce( sum );
// 42
This can go down many layers of abstraction to receive a 'final iteration', where we need only one call to reduce.
var mapReducer = curry(2, function mapReducer(mappingFn, combineFn) {
return function reducer(list, v) {
return combineFn(list, mappingFn(v))
}
})
var filterReducer = curry(2, function filterReducer(predicateFn, combineFn) {
return function reducer(list, v) {
if (predicateFn(v)) return combineFn(list, v)
return list
}
})
var transducer = compose(mapReducer(add1), filterReducer(isOdd))
var list = [1, 3, 4, 6, 9, 12, 13, 16, 21]
list.reduce(transducer(sum), 0) // 42
;``
Monads
A monad is a pattern for pairing data with a set of predictable behaviours that let it interact with other data+behaviour pairings.
function Just(val) {
return { map, chain, ap }
// ************
function map(fn) {
return Just(fn(val))
}
function chain(fn) {
// aka: bind, flatMap
return fn(val)
}
function ap(anotherMonad) {
return anotherMonad.map(val)
}
}
Examples of using the above:
const user1 = Just('Kyle')
const user2 = Just('Susan')
const tuple = curry(2, function tuple(x, y) {
return [x, y]
})
const users = user1.map(tuple).ap(user2)
users.chain(identity) //["Kyle", "Susan"]
Why use them? They have a clean and predictable way of working together. A 'Nothing' monad will return Nothing monads. A 'Maybe' monad either holds a Just or Nothing monad. Creates a nullable value.