Table of Contents
- Chapter 3. Functional Programming with JavaScript
- 3.1 What It Means to Be Functional
- Declare functions with var, let, const
- Add functions to objects:
- Add functions to arrays in JS
- Send function as argument to other functions
- Return function from another function
- Higher-Order functions as arrow function
- 3.2 Imperative Versus Declarative
- 3.3 Functional Concepts
- 3.3.1 Immutability
- Use Object.assign or equivalent arrow function to copy and change content
- Use Array.concat or equivalent spread operator to keep array immutable
- 3.3.2 Pure Functions
- 3.3.3 Data Transformations
- Array.join to combine array
- Array.filter to monidfy array while maintain immutability
- Array.map to produce mutated array
- Array.map to change a single object in an arry of object
- Transform an array into an object by combination using of Object.keys & Array.map
- Transfer an array into values using reduce & reduceRight
- 3.3.4 Higher-Order Functions
- 3.3.5 Recursion
- 3.3.6 Composition
- 3.3.7 Putting It All Together
Chapter 3. Functional Programming with JavaScript
3.1 What It Means to Be Functional
JS functions are 1st-class citizens (i.e. functions can be treated as data. It can be saved, retrieved or flow through app), which means it:
- You can declare functions with
var
,let
,const
keywords - Add functions to objects
- Add functions to arrays
- Parsed as argument to other functions
- Returned from other functions
- Used as Higher-Order functions
Declare functions with var
, let
, const
// declare using traditional function
var log = funciton(message) {
console.log(message);
};
// declare using arrow function
const log = message => {
console.log(message);
};
Add functions to objects:
const obj = { // Prevent it to be overwritten
message: "Functions can be added to objects like variables",
log(message) { // Function is stored in a variable called log
console.log(message);
}
};
obj.log(obj.message);
Add functions to arrays in JS
const message = [
"Functions can be inserted into arrays",
message => console.log(message),
"like variables",
message => console.log(message)
];
// Use functions in array
messages[1](messages[0]); // Functions can be inserted into arrays
messages[3](messages[2]); // Like variables
Send function as argument to other functions
const insideFn = logger => {
logger("Function can be sent to other functions as arguments");
};
insideFn(message => console.log(message)); // an arrow function is parsed as argument
Return function from another function
const createScream = function(logger) {
return function(message) {
logger(message.toUpperCase() + "!!!");
}; // Return a function
};
const scream = createScream(message => console.log(message)); // Constructed a console log method
scream("function can be returned from other functions"); // FUNCTIONS CAN BE RETURNED FROM OTHER FUNCTIONS!!!
Parse/Return argument into/from another argument are feature of higher-order function (i.e. function that either take or return other functions).
Higher-Order functions as arrow function
const createScream = logger => message => {
logger(message.toUpperCase() + "!!!");
}
Where:
-
logger
is the 1st parameter, which is parsed as a function -
message
is the 2nd parameter, that used by 1st parameter (i.e.logger
) -
Function declaration with more than one arrow means it's a higher-order function.
3.2 Imperative Versus Declarative
- Declarative programming: style of (functional) programming where app are structured in a way that prioritizes describing what should happen over defining how it should happen.
- Imperitive programming: style of programming that's only concerned with how to achieve results with codes.
3.3 Functional Concepts
3.3.1 Immutability
In functional program, data is immutable. Instead of changing original data structures, we build changed copies of those data structures and use them.
- In JS, function arguments are references to actural data.
- To create copy, we can use
Object.assign
. - To add object to array, use
Array.concat
instead ofArray.push
Use Object.assign
or equivalent arrow function to copy and change content
Given a color object
let color_lawn = {
title: "lawn",
color: "#00FF00",
rating: 0
};
Create a function to rate color
function rateColor(color, rating) {
color.rating = rating;
return color;
}
console.log(rateColor(color_lawn, 5).rating); // 5
console.log(color_lawn.rating); // 5 (color_lawn was parsed into argument as reference, change is reflected)
Rewrite the function, so parsed argument is immutable
const rateColor = function(color, rating) {
return Object.assign({}, color, {rating: rating}); // rating is overwritten
};
console.log(rateColor(color_lawn, 5).rating); // 5
console.log(color_lawn.rating); // 0
The function can be further simplified using arrow funciton
const rateColor = (color, rating) => (
{
...color,
rating
}
)
- This function is exactly the same as
Object.assign
(Very common in later React programming)
Use Array.concat
or equivalent spread operator to keep array immutable
let list = [{ title: "Rad Red" }, { title: "Lawn" }, { title: "Party Pink" }];
Add colors to array using Array.push
, which will change original data
const addColor = function(title, colors) {
colors.push({ title: title });
return colors;
};
console.log(addColor("Glam Green", list).length); // 4
console.log(list.length); // 4
Use Array.concat
to keep original data immutable
const addColor = (title, array) => array.concat({ title });
console.log(addColor("Glam Green", list).length); // 4
console.log(list.length); // 3
Use equivalent spread operator
const addColor = (title, list) => [...list, { title }]; // This will change or add new object
3.3.2 Pure Functions
Pure function = a function that returns a value that computed based on its arguments.
- Rules of pure function:
- Pure function takes arguments (treat as immutable data) and return a value or another function based on given arguments
- Pure function do not
- cause side effects
- set global variables
- Change any app states.
- Benefit of pure function:
- Pure functions are testable, as they do not change anything outside of its scope. UT on pure functions do not require complicated test setups.
e.g. Impure function
const frederick = {
name: "Frederick Douglass",
canRead: false,
canWrite: false
};
function selfEducate() {
frederick.canRead = true;
frederick.canWrite = true;
return frederick;
}
selfEducate();
console.log(frederick);
// {name: "Frederick Douglass", canRead: true, canWrite: true}
Problem of impure function selfEducate
:
- It do not take any arguments,
- It do not return a vlue or a function.
- It changes a variable outside of its scope:
Frederick
.
Change to pure function
const selfEducate = person => ({
...person,
canRead: true,
canWrite: true
});
console.log(selfEducate(frederick));
console.log(frederick);
// {name: "Frederick Douglass", canRead: true, canWrite: true}
// {name: "Frederick Douglass", canRead: false, canWrite: false}
Summary of Pure function
Pure function is an important core concept of function programming. It make development easier as it does not change application's state. Three rules to follow, when writing functions:
- The function should take in at least 1 argument.
- The function should return a value or another function.
- The function should not change or mutate any of its argument.
3.3.3 Data Transformations
Functional JS has tools for data transformation without altering the original. Two core functions are:
Array.map
Array.reduce
Array.join
to combine array
const schools = ["Yorktown", "Washington & Liberty", "Wakefield"];
console.log(schools.join(", ")); // "Yorktown, Washington & Liberty, Wakefield"
console.log(schools); // (3) ['Yorktown', 'Washington & Liberty', 'Wakefield']
- Original array is not changed.
Array.filter
to monidfy array while maintain immutability
Array.filter
takes a predicate (谓词) as its only argument, and return an array consisting items that fit the prediate criteria
- A predicate is a function that return a Boolean value:
true
orfalse
. Array.filter
invokes this predicate for every item in array (like for loop).- Each item is passed to predicate as an argument
- The return value (
true
orfalse
) is used to decide if the item will be added to new array.
const wSchools = schools.filter(school => school[0] === "W");
console.log(wSchools); // ["Washington & Liberty", "Wakefield"]
const cutSchool = (cut, list) => list.filter(x => x != cut);
console.log(cutSchool("Washington & Liberty", schools).join(", ")); // "Yorktown, Wakefield"
Benefit of Array.filter
:
- good for removing an item from an array; better than
Array.pop
orArray.splice
, asArray.filter
is immutable
Array.map
to produce mutated array
Array.map
method takes a function as its argument, and return mutated items in new array:
- The function will be invoked once for every item in the array, and whatever it returns will be added to new array
const highSchools = schools.map(school => `${school} High School`); // append "High School" to each item
console.log(highSchools.join("\n"));
// Yorktown High School
// Washington & Liberty High School
// Wakefield High School
e.g. Use .map
to transfer array of strings to array containing objects
const highSchools = schools.map(school => ({ name: school}));
console.log(highSchools);
// [
// { name: "Yorktown" },
// { name: "Washington & Liberty" },
// { name: "Wakefield" }
// ]
Array.map
to change a single object in an arry of object
// Object: Create a pure function to change one object in an array of objects
let schools = [
{ name: "Yorktown" },
{ name: "Stratford" },
{ name: "Washington & Liberty" },
{ name: "Wakefield" }
];
const editName = (oldName, name, arr) =>
arr.map(item => {
if (item.name === oldName) {
return {
...item,
name
};
} else {
return item;
}
});
let updatedSchools = editName("Stratford", "HB Woodlawn", schoools);
console.log(updatedSchools[1]); // { name: "HB Woodlawn" }
console.log(schools[1]); // { name: "Stratford" }
// Hence the original array is not changed
The editName
function can be simplified using arrow function
const editName = (oldName, name, arr) => arr.map(item => (item.name === oldName ? {...item, name} : item));
Transform an array into an object by combination using of Object.keys
& Array.map
Object.keys
extract keys from an object as an array.
const schools = {
Yorktown: 10,
"Washington & Liberty": 2,
Wakefield: 5
};
console.log(Object.keys(schools));
// ["Yorktown", "Washington & Liberty", "Wakefield"]
let schoolArray = Object.keys(schools).map(key => ({
name: key,
wins: schools[key]
}))
// [
// {
// name: "Yorktown",
// wins: 10
// },
// {
// name: "Washington & Liberty",
// wins: 2
// },
// {
// name: "Wakefield",
// wins: 5
// }
// ]
Transfer an array into values using reduce
& reduceRight
Array.prototype.reduce()
: method executes a user-supplied “reducer” callback function on each element of the array, in order, passing in the return value from the calculation on the preceding element. The final result of running the reducer across all elements of the array is a single value. OverallArray.reduce
can be used to reduce an array to a single value/object.- [
Array.prototype.reduceRight()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduceRight): Works same as
.reduce()`, but work from end of array.
Find maximum number in an array of numbers
const ages = [21, 18, 42, 40, 64, 63, 34];
const maxAge = ages.reduce(
(max, age) => { // "reducer" callback function to be applied on each element of the array
console.log(`${age} > ${max} = ${age > max}`);
if (age > max) {
return age;
} else {
return max;
}
}
, 0); // 0 is initialValue
// 21 > 0 = true
// 18 > 21 = false
// 42 > 21 = true
// 40 > 42 = false
// 64 > 42 = true
// 63 > 64 = false
// 34 > 64 = false
The maxAge
above can be convereted into a common JS arrow function that calculate max value in any array of numbers:
const max = ages.reduce((max, value) => (value > max ? value : max), 0);
Transform an array into an object
const colors = [
{
id: "xekare",
title: "rad red",
rating: 3
},
{
id: "jbwsof",
title: "big blue",
rating: 2
},
{
id: "prigbj",
title: "grizzly grey",
rating: 5
},
{
id: "ryhbhsl",
title: "banana",
rating: 1
}
];
const hashColors = colors.reduce((hash, {id, title, rating}) => {
hash[id] = {title, rating};
return hash;
}, {});
console.log(hashColors);
// {
// "xekare": {
// title:"rad red",
// rating:3
// },
// "jbwsof": {
// title:"big blue",
// rating:2
// },
// "prigbj": {
// title:"grizzly grey",
// rating:5
// },
// "ryhbhsl": {
// title:"banana",
// rating:1
// }
// }
Where:
- The initial value for the hash is empty object.
- During each iteration, the callback function adds a new key to the hash using
{}
and sets the value for that key toid
field of the array.
Transform arrays into another arrays using reduce
Objective: reduce an array of distinct values.
const colors = ["red", "red", "green", "blue", "green"];
const uniqueColors = colors.reduce(
(unique, color) => unique.indexOf(color) !== -1 ? unique : [...unique, color], // when the new array does not already contain a specific color, it add the new color
[]
)
console.log(uniqueColors); // ["red", "green", "blue"]
Todo: understand reduce
and map
fully and proficiently.
3.3.4 Higher-Order Functions
Higher-order function: functions can manipulate other functions (i.e. take function as argument, or return function)
- e.g. of Higher-order functions:
Array.map
,Array.filter
,Array.reduce
e.g. Implement/Create a higher-order function
const invokeIf = (condition, fnTrue, fnFalse) => // takes in functions for fnTrue, and fnFalse
condition ? fnTrue(): fnFalse();
const showWelcome = () => console.log("Welcome!!!");
const showUnauthorized = () => console.log("Unauthorized!!!");
invokeIf(true, showWelcome, showUnauthorized); // "Welcome!!!"
invokeIf(false, showWelcome, showUnauthorized); // "Unaurhtorized!!!"
Currying: a funcitonal technique used in higher-order function. It's a process in functional programming in which we can transform a function with multiple arguments into a sequence of nesting functions.
- It returns a new function that expects the next argument inline.
- Good to handle complexities associated with asynchronicity in JS.
e.g. Currying
const userLogs = userName => message => // Higher-order function using Currying
console.log(`${userName} -> ${message}`);
const log = userLogs("grandpa23"); // `log` function is produced from `userLogs`. Everytime the `log` function is used, "grandpa23" is prepended to the message.
log("attempted to load 20 fake members");
getFakeMembers(20).then(
members => log(`successfully loaded ${members.length} members`),
error => log("encountered an error loading members")
);
3.3.5 Recursion
Recursion: technique that create functions that recall themselves.
- Works well with async processes.
e.g. Recursive programming to solve problem: count down from 10
const countdown = (value, fn) => {
fn(value);
return value > 0 ? countdown(value-1, fn) : value; // once value == 0, countdown will return value all theway back up the call stack
};
countdown(10, value => console.log(value));
const countdown = (value, fn, delay = 1000) => {
fn(value);
return value > 0
? setTimeout( () => countdown(value - 1, fn, delay), delay) : value; // wait 1 sec, then recalling itself, thus creating a clock
};
const log = value => console.log(value);
countdown(10, log);
Recursion for searching data structure
Recursion is a good technique for searching data structure. usage e.g.:
- iterate through subfolders until a folder that contains required file is identified
- iterate through HTML DOM until an required element is found
e.g. iterate into an object to retrieve a nested value
const dan = {
type: "person",
data: {
gender: "male",
info: {
id: 22,
fullname: {
first: "Dan",
last: "Deacon"
}
}
}
};
let deepPick = (fields, object = {}) => {
const [first, ...remaining] = fields.split(".");
return remaining.length
? deepPick(remaining.join("."), object[first]) : object[first];
}
deepPick("type", dan); // "person"
// For `deepPick("type", dan);`, `"type".split(".")` returns `["type"]`, hence the `remaining` length is 0. Therefore, we can return `object[first]` directly
deepPick("data.info.fullname.first", dan); // "Dan"
// First Iteration
// first = "data"
// remaining.join(".") = "info.fullname.first"
// object[first] = { gender: "male", {info} }
// Second Iteration
// first = "info"
// remaining.join(".") = "fullname.first"
// object[first] = {id: 22, {fullname}}
// Third Iteration
// first = "fullname"
// remaining.join("." = "first"
// object[first] = {first: "Dan", last: "Deacon" }
// Finally...
// first = "first"
// remaining.length = 0
// object[first] = "Deacon"
3.3.6 Composition
How functional programming works:
- break logic into small, pure functions. Each function focus on specific tasks.
- Combine these functions (paralle or serial) into larger functions, and further up until entire application.
Many techniques/patterns are used in composition, chaining is one of them.
Chaining
Chaining: Chain functions together using .
notation. Each called function act on return value of previous invoked function.
- Used when want to apply function on returned object
const template = "hh:mm:ss tt";
const clockTime = template
.replace("hh", "03")
.replace("mm", "33") // act on return of `.replace("hh", "03")
.replace("ss", "33")
.replace("tt", "PM");
console.log(clockTime); // "03:33:33 PM"
Use compose
to pipe functions
Problem to solve: traditional way of piping (combining) 2 separate functions (as shown below) is not easy to scale.
const both = date => appendAMPM(civilianHours(date));
Compose is a good way of apply function into arguments.
compose
is a higher-order function, which takes functions as arguments and returns a single value
Implementation of compose
(not given by JS by default):
const compose = (...fns) => arg =>
fns.reduce((composed, f) => f(composed), arg);
...fns
spread operator turn function arguments into an arrayfns
- Through currying, returned function
fns.reduce(...)
expects one argumentarg
, as initial Value of.reduce()
- The reducer callback function
(composed, f) => f(composed)
means for function array[func1, func2, func3, ...]
, the reducer will produce a function...func3(func2(func1(arg)))
. - The order of function array
fns
is from inner to outer.
e.g. of using compose
const both = compose(
civilianHours,
appendAMPM
);
both(new Date());
Where:
civilianHours
is inner,appendAMPM
is outer.
Overall, for compose
:
- Pro: easier to scale/construct functional call from inner to outer
- Con: quickly become more complex when handling multiple arguments
3.3.7 Putting It All Together
Core concepts of functional programminig:
- Immutability
- Pure functions
- Data Transforms
- Higher-Order Functions
- Recursion
- Composition
Now they can be put together to build proper JS app.
Objective: build a ticking clock, which
- Display hours, minutes, seconds, and time of day in civilian time.
- Each field have double digits.
- Clock must tick and change diplay every second
Imperative solution design:
- Use
Window setInterval()
to calls a function every 1000ms - The called function will clear console, and display the clock
- The displayed clock is returned from another function
getClockTime
that produce clock string
// Log Clock Time every Second
setInterval(logClockTime, 1000);
function logClockTime() {
console.clear();
// Get Time string as civilian time
let time = getClockTime();
console.log(time);
}
function getClockTime() {
// Get Current Time and return as string with correct format
let date = new Date();
// Serialize clock time
let time = {
hours: date.getHours(),
minutes: date.getMinutes(),
seconds: date.getSeconds(),
ampm: "AM"
};
// Convert to civilian time
if (time.hours == 12) {
time.ampm = "PM";
} else if (time.hours > 12) {
time.hours = -12;
time.amap = "PM";
}
// Make hours value double digit
if (time.hours < 10) {
time.hours = "0" + time.hours;
}
// Make minutes value double digit
if (time.minutes < 10) {
time.minutes = "0" + time.minutes;
}
// Make seconds value double digit
if (time.seconds < 10) {
time.seconds = "0" + time.seconds;
}
// Format the clock time as a string "hh:mm:ss tt"
return time.hours + ":" + time.minutes + ":" + time.seconds + " " + time.ampm;
}
- Problem of this implementation:
getClockTime
tooks too much responsiblities- Entire implementation is large and complicated.
Good pratice of functional programming:
- Use Functions over values whenever possible.
const compose = (...fns) => arg =>
fns.reduce((composed, f) => f(composed), arg);
// Define funtions to get values and manage console
const oneSecond = () => 1000;
const getCurrentTime = () => new Date();
const clear = () => console.clear();
const log = message => console.log(message);
// Define functions to transform data
const serializeClockTime = date => ({ // Hence, date is immutable
hours: date.getHours(),
minutes: date.getMinutes(),
seconds: date.getSeconds()
});
const civilianHours = clockTime => ({ // Hence, clockTime is immutable
...clockTime,
hours: clockTime.hours > 12? clockTime.hours - 12: clockTime.hours
});
const appendAMPM = clockTime => ({ // As in functional programming, args are immutable, hence change in other functions does not affect this one
...clockTime,
ampm: clockTime.hours >= 12 ? "PM" : "AM"
})
// Define reusable higher-order functions
const display = target => time => target(time);
const formatClock = format => time =>
format
.replace("hh", time.hours)
.replace("mm", time.minutes)
.replace("ss", time.seconds)
.replace("tt", time.ampm);
const prependZero = key => clockTime => ({
...clockTime,
key: clockTime[key] < 10 ? "0" + clockTime[key] : clockTime[key]
})
// Compose
const convertToCivilianTime = clockTime =>
compose (
appendAMPM,
civilianHours
) (clockTime);
const doubleDigits = civilianTime =>
compose(
prependZero("hours"),
prependZero("minutes"),
prependZero("seconds")
) (civilianTime);
const startTicking = () =>
setInterval(
compose(
clear,
getCurrentTime,
serializeClockTime,
convertToCivilianTime,
doubleDigits,
formatClock("hh:mm:ss tt"),
display(log)
), oneSecond()
);
startTicking();
Todo: Fix bugs in this code. Most of it works, but not all features are implemented correctly.