preface

Functional programming is a development style that emphasizes the use of functions. The function here is a mathematical function that is a mapping of variables, and the value of a function depends only on the parameters of the function, not on other states. Its goal is to use functions to abstract the flow of control and operations acting on the data, thereby eliminating side effects and reducing changes to state in the program.

Functional programming has four basic concepts:

  • Declarative programming
  • Pure functions
  • Reference transparent
  • immutability

Functional programming

Declarative programming

Functional programming is the declarative programming paradigm, concerned only with mapping data, as opposed to the imperative programming paradigm. Imperative programming is an abstraction for computer hardware, with variables, assignment statements, expressions, and control statements, etc., while functional programming is a mathematical abstraction that describes computation as an expression to solve.

For example: To compute the square of all the numbers in an array, the imperative programming is as follows:

const numArr = [1.2.3.4.5.6.7.8.9];
for (var i = 0; i < numArr.length; i++) {
    numArr[i] = Math.pow(numArr[i], 2);
}
console.log(numArr); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
Copy the code

As you can see, imperative programming tells the computer specifically how to perform a task.

Declarative programming, by contrast, separates the description of a program from its evaluation. It focuses on how various expressions can be used to describe program logic without necessarily specifying its control flow or changes in state. Typical declarative programming is SQL. SQL statements are made up of assertions that describe what the results of a query should look like, abstracting the internal mechanisms of data retrieval.

If you use declarative programming to solve the above problem, you only need to focus on the behavior applied to each array element, leaving the loop part to the rest of the system to control, because loops are an important command control structure that is difficult to reuse and insert into other operations.

[1.2.3.4.5.6.7.8.9].map(num= > Math.pow(num, 2))
// [1, 4, 9, 16, 25, 36, 49, 64, 81]
Copy the code

Pure functions

The use of pure functions is a prerequisite for functional programming. Pure functions have the following properties:

  • The same input always yields the same output, which is independent of external state during function evaluation or between calls.
  • Does not cause changes beyond its scope, such as modifying global objects or parameters passed by references.

Any function that does not meet the above conditions is “impure”.

let num = 0;
function increment() {
    reuturn num++;
}
Copy the code

Obviously the increment function is impure because it reads and modifies an external variable num. In fact, pure functions are hard to use in JavaScript, a language full of dynamic behavior and change. But functional programming does not limit all state changes in practice. It simply provides a framework to help manage and reduce mutable state, while being able to separate pure functions from their impure parts.

Look at the following example:

function getPersonFromId (id) {
    const person = localStorage.getItem(id);
    if(persion ! = =null) {
        document.querySelector(` #${elementId}`).innerHTML = `${person.name} born in ${person.birthday}`;
    } else {
        throw new Error('person not found! ')
    }
}
getPersonFromId('440111199 * * * * * * * * *');
Copy the code

GetPersonFromId has the following exceptions to pure functions:

  • Function with an external variable (localStorage), but this parameter is not declared in the function signature. At any point in time, this reference could benull“, resulting in completely different results and breaking the “purity” of the program.
  • The global variableelementIdIt can change when a function is called, and it is difficult to control.
  • HTMLThe element is also a global variable, which may not be the same the next time it is called.
  • If the person is not found, the function throws an exception that causes the entire program to fall back on the stack and end abruptly.

To change getPersonFromId to a pure function, first separate the display and fetch actions. Of course, side effects from external interactions and DOM interactions are inevitable, but they can at least be made more manageable by separating them from the main logic. This requires the Use of the Cremation technique, which can be used to partially pass function arguments in order to reduce the parameters of the function to one.

const find = R.curry((localStorage, id) = > {
    const person = localStorage.getItem(id);
    if(person ! = =null) {
        throw new Error('person not found');
    }
    return person;
});
const showMessage = (person_= > `${person.name} born in ${person.birthday}`;
const append = R.curry((elementId, info) = > {
    document.querySelector(elementId).inner = info
})

const getPersonFromId = R.compose(
                            append('#person'),
                            csv,
                            find(localStorage));
getPersonFromId('440111199 * * * * * * * * *');
Copy the code

Although it is a small change, it already shows several advantages:

  • There are three reusable components.
  • Spreading out a long function greatly reduces the amount of code to maintain.
  • The declarative style provides a clear view of the steps the program needs to perform, improving code readability.
  • withHTMLThe interaction is moved to a separate function, separating the impurity from the pure function.

Currization Currization is the technique of converting a multi-parameter function into a sequence of unary functions by suspending or delaying the execution of the function before all parameters are supplied. It requires that all parameters be clearly defined. So, when the call is missing arguments, it returns a new function, waiting for the rest of the arguments to be supplied before actually running.

In functional programming languages, currization is a native feature and is part of the function definition. However, JavaScript does not natively support currification, because calling a non-Currification function with missing arguments causes the missing argument to become undefined. For example, if you define a function f(a, b, c) and only pass a when called, the JavaScript runtime call mechanism sets b and C to undefined.

So you need to write some code in JavaScript to start currification. Take, for example, the following manual Coriolization function for binary arguments

function curry2(fn) {
    return function (firstArg) {
        return function (secondArg) {
            return fn(firstArg, secondArg)
        }
    }
}
Copy the code

As you can see, currization is really just a lexical scope where the function returned is nothing more than a simple nested function wrapper that receives subsequent arguments.

Reference transparency and replaceability

Reference transparency is the correct way to define a pure function. Purity in this sense indicates the mapping between the parameters and return values of a function. Therefore, a function is reference-transparent if it consistently produces the same result for the same input. Previous examples:

let num = 0;
function increment() {
    reuturn num++;
}
Copy the code

To make its reference transparent, it needs to remove the dependent external variable state and make it an explicitly defined parameter in the function signature.

const increment = (num) = > ++num;
Copy the code

After that, it becomes a pure function that returns the same output every time for the same input. This functional feature is sought because it not only makes the code easier to test, but also makes it easier to reason about the entire program.

Reference transparency comes from a mathematical concept, but programming languages behave differently from mathematical functions, so reference transparency has to be implemented by us. Building such programs is easier to reason about because you can form a model of the state system in mind and rewrite or replace it to achieve the desired output. In particular, suppose that any program can be defined as a set of functions that, for a given input, produce an output, can be expressed as:

Program = [Input] + [fun1, fun2, fun3, ...]  -> OutputCopy the code

If [fun1, fun2, fun3… Is pure, so you can easily rewrite the program with the values generated by it [val1, val2, val3…]. Without changing the outcome. Here’s an example:

const input = [10.20.30];
const average = (arr) = > divide(sum(arr), size(arr));
average(input); / / 20
Copy the code

The functions sum and size are reference-transparent, and this expression can be easily overridden for the following input.

const average = divide(60.3); / / 20
Copy the code

Because Devide is also pure, it can be rewritten using its mathematical notation, so the average is always 60/3=20 for the current input.

Store immutable data

Immutable data is data that cannot be changed after being created. All of JavaScript’s basic types are essentially immutable, but reference types are mutable, so managing object state is a big problem.

Treating objects as Numbers In functional programming, values are made of immutable types, such as primitive strings and numbers. If you can treat objects as values, you don’t have to worry about them being tampered with.

ES6 uses the const keyword to create immutable variables

const pi = 3.14;
pi = 3.15; //Uncaught TypeError: Assignment to constant variable.
Copy the code

But this is not enough, because it does not prevent the internal state of the object from changing.

const piObj = {
    pi: 3.14
}
piObj.pi = 3.15; // ok
Copy the code

A good approach is to adopt the value object pattern, which means that its equality is not dependent on identification or reference, but is based only on its value, and its state is immutable once declared. One way to implement value objects in JavaScript is to use functions to guarantee access to internal state, exposing a portion of the method to the caller by returning an interface.

function person (name, contry) {
    let _name = name;
    let _contry = contry;
    return {
        getName: () = > _name;
        getContry: () = > _contry;
        toString: () = > _name + ' from ' _contry
}

const person1 = person('zhang'.'china');
person1.toString(); // 'zhang from china'
Copy the code

In addition, JavaScript has an internal mechanism that controls the writable property. For example, the object.freeze () function can set this property to false to prevent Object state changes.

const piObj = Object.freeze({ pi: 3.14 })
piObj.pi = 3.15 / / no
Copy the code

But object.freeze () is a shallow operation. To solve the deep attribute immutable problem, you need to manually freeze the nested structure of the object

const isObj = (val) = > val && typeof val === 'object')
function deepFreeze (obj) {
    if (isObj(obj) && !Object.isFrozen(obj)) {
        Object.keys(obj).forEach(name= > deepFreeze(obj[name]);
        Object.freeze(obj);
    }
    return obj;
}
Copy the code

Using the deepFreeze function to locate and modify the object graph can enhance the immutable level in your code, but it is unrealistic to create a program that is never mutable. It is therefore possible to create a new object from the original object and return a new object each time the method is called.

Lenses are called functional references, and are the solution in functional programming for accessing and immutably manipulating state-data type properties. In essence, Lenses are similar to a copy-on-write strategy that uses an internal storage unit that can properly manage and assign state.

The ramda.js library already implements this scheme, such as using R.lenprop to create a Lens that wraps the PI property of piObj:

const piLens = R.lenseProp('pi');
R.view(piLens, piObj); / / 3.14;

const newPi = R.set(piLens, '3.15', piObj);
newPi.pi; / / 3.15
piObj.pi; / / 3.14
Copy the code

Lense is valuable because it provides a less cumbersome mechanism for manipulating objects, even objects left over from history or beyond control.

// Lenses modify nested properties
const person = {
    address: {
        country: 'china'.city: 'guangzhou'}}const cityPath = ['address'.'city'];
const cityLens = R.lens(R.path(cityPath), R.assocPath(cityPath));
R.view(cityLens, person); // 'guangzhou'

const newPerson = R.set(cityLens, person,  'shenzhen');
Copy the code

How to do functional programming

The breakdown of complex tasks

At a macro level, functional programming is really an interaction between decomposition and composition. For example getPersonFromId, split it into find, CSV, and Append. The concept of modularity in functional programming is closely related to a single responsibility. That is, functions should have a single purpose, such as the Average function. Purity and referential transparency force you to think that in order to compose functions together, they must be consistent in the form of their inputs and outputs.

The R.compose function used above is composed. A combination of two functions is a new function that takes the output of one function and passes it to another function. Suppose there are two functions f and g, whose relation f * g = f(g(x)), where a loosely-coupled, type-safe relation is constructed between the return value of the g function and the parameters of F. Two functions can be combined only if they agree on the number and type of arguments.

In essence, function composition is an overall process of organizing simple tasks that have been decomposed into complex behaviors. For example:

const words = 'we are family';
const explode = (str) = > str.split(/\s+/);
const count = (arr) = > arr.length;
const countWords = R.compose(count, explode);
countWords(words); / / 3
Copy the code

The call to the countWords function triggers the execution of explode and passes its output to count to calculate the length of the array. Composition links inputs and outputs to create function channels. This code does not trigger the evaluation until countWords is called. The result of the combination is another function called countWords waiting for a specified argument to be called. The power of functional composition is to separate the description of a function from its evaluation.

The realization of the composer

functon compose(. args) {
    const start = args.length - 1;
    return function () {
        let i = start;
        let result = args[start].apply(this, args);
        while(i--){
            return = args[i].call(this, result);
        returnresult; }}Copy the code

One of the benefits of using the Ramda function library is that all functions have been properly currified, making it more versatile when combining function pipelines. Here’s another example:

const students = ['zhao'.'qian'.'sun'.'li'];
const scores = [90.88.97.80];
Copy the code

Find the highest achievers in the class

const smarTestStudent = R.compose(
    R.head, // Get the first element
    R.pluck(0), Build an array by extracting the elements of the specified index. Here 0 means to extract the student name
    R.reverse, // Invert the array
    R.sortBy(R.prop[1]), // Sort in ascending order by the specified attribute
    R.zip);
smarTestStudent(students, scores); // 'sun'
Copy the code

Use chains to process data

Functional chain a program that computes lazily, meaning that it is executed when needed. This saves CPU by avoiding executing code that may never be used. If you’ve written some Jquery code, you’re familiar with chaining. Chains are a series of calls to functions that share a common object return value. Like combinations, chains are useful for writing succinct code, and they are often used in both functional and reactive JavaScript libraries.

Example: average grades in higher grades (that is, grade 4 and above)

const school = [
    { grade: 1.score: 92 },
    { grade: 2.score: 93 },
    { grade: 3.score: 94 },
    { grade: 4.score: 95 },
    { grade: 5.score: 96 },
    { grade: 6.score: 97}]Copy the code

Using functional thinking, this problem can be broken down into three main steps:

  • Screen for appropriate grades
  • Get grades for the corresponding grade
  • Average them out

Combine the functions of these steps using Lodash.

_.chain(school)
    .filter(grade= > grade.grade > 3)
    .pluck('score')
    .average()
    .value()
Copy the code

The _. Chain function can add the state of an input object to link together operations that convert these inputs to the desired output. Instead of simply wrapping an array around an _(…) Objects, unlike objects, are powerful in that they can be linked to any function in a sequence. Although this is a complex program, it still avoids creating any variables and effectively eliminates all loops.

Another benefit of using _.chain is that you can create complex programs with lazy computing power that don’t actually do anything until value() is called. This can have a huge impact on the program because you can skip running all the functions without needing the results. Each function in the chain processes the new array built by the previous function in an immutable way.

If you’ve ever worked with SQL, you’ll notice some similarities. For example, Person:

id name age city
1 zhang 23 guangzhou
2 li 24 shenzhen
3 qian 25 shanghai
SELECT p.name, p.age FROM Person
WHERE p.age > 24 and p.city IS NOT 'shanghai'
GROUP BY p.name, p.age
Copy the code

Lodash supports a feature called mixins that can be used to extend new functions to the core library and make them connect in the same way:

_.mixins({'select', _.pluck,
          'from',   _.chain,
          'where',  _.filter,
          'groupBy',_.sortByOrder})
Copy the code

You can then write an SQL-like statement

_.from(persons)
    .where(p= > p.age > 24&& p.city ! = ='shanghai')
    .groupBy(['name'.'age'])
    .select('name'.'age')
    .value();
Copy the code

At the end

Reference for this article:

  • “JavaScript Functional Programming”
  • JavaScript Functional Programming Guide
  • Functional programming is north

For more articles, please go to Github, if you like, please click star, which is also a kind of encouragement to the author.