2 chap03
Jason Zhu edited this page 2022-01-09 18:23:46 +11:00

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 of Array.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:

  1. The function should take in at least 1 argument.
  2. The function should return a value or another function.
  3. 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 or false.
  • 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 or false) 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 or Array.splice, as Array.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. Overall Array.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 to id 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:

  1. break logic into small, pure functions. Each function focus on specific tasks.
  2. 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 array fns
  • Through currying, returned function fns.reduce(...) expects one argument arg, 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.