Home  >  Article  >  Web Front-end  >  An in-depth analysis of higher-order functions, currying and combining functions in JavaScript

An in-depth analysis of higher-order functions, currying and combining functions in JavaScript

青灯夜游
青灯夜游forward
2021-09-22 20:10:451981browse

This article will take you to understand functional programming in JavaScript, introduce higher-order functions, currying and combination functions, as well as common functional functions. I hope it will be helpful to everyone!

An in-depth analysis of higher-order functions, currying and combining functions in JavaScript

Object-oriented programming and Functional programming are two very different programming paradigms, they have their own rules and advantages shortcoming.

However, JavaScript does not always follow one rule, but is right in the middle of these two rules. It provides some aspects of ordinary OOP languages, such as classes, objects, inheritance, etc. But at the same time, it also gives you some concepts of functional programming, such as higher-order functions and the ability to compose them.

Higher-order functions

We start with the most important of the three concepts: higher-order functions.

Higher-order functions mean that functions are not just one that can be defined and called from code, you can actually use them as allocatable entities. If you've used some JavaScript, this isn't surprising. Assigning anonymous functions to constants is very common.

const adder = (a, b) => {
  return a + b
}

The above logic is not valid in many other languages, being able to allocate functions like integers is a very useful tool, in fact most of the topics covered in this article are by-products of this function.

Benefits of higher-order functions: encapsulation behavior

With higher-order functions, we can not only allocate functions as above, but also use them as Parameter passing. This opens the door to creating a constantly dynamic code base on which complex behavior can be reused by passing it directly as a parameter.

Imagine working in a pure object-oriented environment and you want to extend the functionality of a class to complete a task. In this case, you might use inheritance by encapsulating that implementation logic in an abstract class and then extending it into a set of implementation classes. This is perfect OOP behavior and it works, we:

  • Created an abstract structure to encapsulate our reusable logic
  • Created a secondary construct
  • We reused the original class and extended it

Now, what we want is to reuse the logic, we can simply extract the reusable logic into a function, and then This function is passed as a parameter to any other function. This way, we can save some "boilerplate" process, because we are just creating the function.

The code below shows how to reuse program logic in OOP.

//Encapsulated behavior封装行为stract class LogFormatter {
  
  format(msg) {
    return Date.now() + "::" + msg
  } 
}

//重用行为
class ConsoleLogger extends LogFormatter {
  
  log(msg) {
    console.log(this.format(msg))
  }  
}

class FileLogger extends LogFormatter {

  log(msg) {
    writeToFileSync(this.logFile, this.format(msg))
  }
}

The second representation is to extract the logic into a function where we can mix and match to easily create what we need. You can keep adding more formatting and writing features, and then just mix them together with a single line of code:

// 泛型行为抽象
function format(msg) {
  return Date.now() + "::" + msg
}

function consoleWriter(msg) {
  console.log(msg)
}

function fileWriter(msg) {
  let logFile = "logfile.log"
  writeToFileSync(logFile, msg)
}

function logger(output, format) {
  return msg => {
    output(format(msg))
  }
}
// 通过组合函数来使用它
const consoleLogger = logger(consoleWriter, format)
const fileLogger = logger(fileWriter, format)

Both approaches have advantages, and both are very effective, and neither one is the best. Just to show the flexibility of this approach, we have the ability to pass behaviors (i.e. functions) as arguments as if they were primitive types (like integers or strings).

Benefits of higher-order functions: concise code

A good example of this benefit is the Array method, such as forEach , map, reduce and so on. In non-functional programming languages ​​such as C, iterating over array elements and transforming them requires the use of a for loop or some other looping construct. This requires us to write code in a specified way, that is, the requirements describe the process in which the cycle occurs.

let myArray = [1,2,3,4]
let transformedArray = []

for(let i = 0; i < myArray.length; i++) {
  transformedArray.push(myArray[i] * 2) 
}

The above code mainly does:

  • Declare a new variable i, which will be used as the index of myArray, Its value ranges from 0 to the length of myArray
  • For each value of i, the length of myArray The values ​​are multiplied at position i and added to the transformedArray array.

This approach is effective and relatively easy to understand, however, the complexity of this logic increases as the complexity of the project increases, and so does the cognitive load. However, it is easier to read like this:

const double = x => x * 2;

let myArray = [1,2,3,4];
let transformedArray = myArray.map(double);

Compared to the first way, this way is easier to read, and because the logic is hidden in two functions (map and double), so you don't have to worry about understanding how they work. You could also hide the multiplication logic inside the function in the first example, but the traversal logic must be present, which adds some unnecessary reading obstruction.

Currying

Function currying is to transform a function that accepts multiple parameters into a function that accepts a single parameter (the first parameter of the original function A function that takes one parameter) and returns a new function that accepts the remaining parameters and returns a result. Let's look at an example:

function adder(a, b) {
  return a + b
}

// 变成
const add10 = x => adder(a, 10)

Now, if all you want to do is add 10 to a range of values, you can call add10 instead of each time Both call adder with the same second argument. This example may seem silly, but it embodies the ideal of currying.

你可以将柯里化视为函数式编程的继承,然后按照这种思路再回到logger的示例,可以得到以下内容:

function log(msg, msgPrefix, output) {
  output(msgPrefix + msg)
} 

function consoleOutput(msg) {
  console.log(msg)
}

function fileOutput(msg) {
  let filename = "mylogs.log"
  writeFileSync(msg, filename)
}

const logger = msg => log(msg, ">>", consoleOutput);
const fileLogger = msg => log(msg, "::", fileOutput);

log的函数需要三个参数,而我们将其引入仅需要一个参数的专用版本中,因为其他两个参数已由我们选择。

注意,这里将log函数视为抽象类,只是因为在我的示例中,不想直接使用它,但是这样做是没有限制的,因为这只是一个普通的函数。 如果我们使用的是类,则将无法直接实例化它。

组合函数

函数组合就是组合两到多个函数来生成一个新函数的过程。将函数组合在一起,就像将一连串管道扣合在一起,让数据流过一样。

在计算机科学中,函数组合是将简单函数组合成更复杂函数的一种行为或机制。就像数学中通常的函数组成一样,每个函数的结果作为下一个函数的参数传递,而最后一个函数的结果是整个函数的结果

这是来自维基百科的函数组合的定义,粗体部分是比较关键的部分。使用柯里化时,就没有该限制,我们可以轻松使用预设的函数参数。

代码重用听起来很棒,但是实现起来很难。如果代码业务性过于具体,就很难重用它。如时代码太过通用简单,又很少人使用。所以我们需要平衡两者,一种制作更小的、可重用的部件的方法,我们可以将其作为构建块来构建更复杂的功能。

在函数式编程中,函数是我们的构建块。每个函数都有各自的功能,然后我们把需要的功能(函数)组合起来完成我们的需求,这种方式有点像乐高的积木,在编程中我们称为 组合函数。

看下以下两个函数:

var add10 = function(value) {
    return value + 10;
};
var mult5 = function(value) {
    return value * 5;
};

上面写法有点冗长了,我们用箭头函数改写一下:

var add10 = value => value + 10;
var mult5 = value => value * 5;

现在我们需要有个函数将传入的参数先加上 10 ,然后在乘以 5, 如下:

现在我们需要有个函数将传入的参数先加上 10 ,然后在乘以 5, 如下:

var mult5AfterAdd10 = value => 5 * (value + 10)

尽管这是一个非常简单的例子,但仍然不想从头编写这个函数。首先,这里可能会犯一个错误,比如忘记括号。第二,我们已经有了一个加 10 的函数  add10 和一个乘以 5 的函数 mult5 ,所以这里我们就在写已经重复的代码了。

使用函数 add10mult5 来重构 mult5AfterAdd10

var mult5AfterAdd10 = value => mult5(add10(value));

我们只是使用现有的函数来创建 mult5AfterAdd10,但是还有更好的方法。

在数学中, f ∘ g 是函数组合,叫作“f 由 g 组合”,或者更常见的是 “f after g”。 因此 (f ∘ g)(x) 等效于f(g(x)) 表示调用 g 之后调用 f

在我们的例子中,我们有 mult5 ∘ add10 或 “add10 after mult5”,因此我们的函数的名称叫做 mult5AfterAdd10。由于Javascript本身不做函数组合,看看 Elm 是怎么写的:

add10 value =
    value + 10
mult5 value =
    value * 5
mult5AfterAdd10 value =
    (mult5 << add10) value

Elm 中 << 表示使用组合函数,在上例中 value 传给函数 add10 然后将其结果传递给 mult5。还可以这样组合任意多个函数:

f x =
   (g << h << s << r << t) x

这里 x 传递给函数 t,函数 t 的结果传递给 r,函数 t 的结果传递给 s,以此类推。在Javascript中做类似的事情,它看起来会像 g(h(s(r(t(x))))),一个括号噩梦。

常见的函数式函数(Functional Function)

函数式语言中3个常见的函数:Map,Filter,Reduce

如下JavaScript代码:

 for (var i = 0; i < something.length; ++i) {
    // do stuff
 }

这段代码存在一个很大的问题,但不是bug。问题在于它有很多重复代码(boilerplate code)。如果你用命令式语言来编程,比如Java,C#,JavaScript,PHP,Python等等,你会发现这样的代码你写地最多。这就是问题所在

现在让我们一步一步的解决问题,最后封装成一个看不见 for 语法函数:

先用名为 things 的数组来修改上述代码:

var things = [1, 2, 3, 4];
for (var i = 0; i < things.length; ++i) {
    things[i] = things[i] * 10; // 警告:值被改变!
}
console.log(things); // [10, 20, 30, 40]

这样做法很不对,数值被改变了!

在重新修改一次:

var things = [1, 2, 3, 4];
var newThings = [];
for (var i = 0; i < things.length; ++i) {
    newThings[i] = things[i] * 10;
}
console.log(newThings); // [10, 20, 30, 40]

这里没有修改things数值,但却却修改了newThings。暂时先不管这个,毕竟我们现在用的是 JavaScript。一旦使用函数式语言,任何东西都是不可变的。

现在将代码封装成一个函数,我们将其命名为 map,因为这个函数的功能就是将一个数组的每个值映射(map)到新数组的一个新值。

var map = (f, array) => {
    var newArray = [];
    for (var i = 0; i < array.length; ++i) {
        newArray[i] = f(array[i]);
    }
    return newArray;
};

函数 f 作为参数传入,那么函数 map 可以对 array 数组的每项进行任意的操作。

现在使用 map 重写之前的代码:

var things = [1, 2, 3, 4];
var newThings = map(v => v * 10, things);

这里没有 for 循环!而且代码更具可读性,也更易分析。

现在让我们写另一个常见的函数来过滤数组中的元素:

var filter = (pred, array) => {
    var newArray = [];
for (var i = 0; i < array.length; ++i) {
        if (pred(array[i]))
            newArray[newArray.length] = array[i];
    }
    return newArray;
};

当某些项需要被保留的时候,断言函数 pred 返回TRUE,否则返回FALSE。

使用过滤器过滤奇数:

var isOdd = x => x % 2 !== 0;
var numbers = [1, 2, 3, 4, 5];
var oddNumbers = filter(isOdd, numbers);
console.log(oddNumbers); // [1, 3, 5]

比起用 for 循环的手动编程,filter 函数简单多了。最后一个常见函数叫reduce。通常这个函数用来将一个数列归约(reduce)成一个数值,但事实上它能做很多事情。

在函数式语言中,这个函数称为 fold

var reduce = (f, start, array) => {
    var acc = start;
    for (var i = 0; i < array.length; ++i)
        acc = f(array[i], acc); // f() 有2个参数
    return acc;
});

reduce函数接受一个归约函数 f,一个初始值 start,以及一个数组 array

这三个函数,map,filter,reduce能让我们绕过for循环这种重复的方式,对数组做一些常见的操作。但在函数式语言中只有递归没有循环,这三个函数就更有用了。附带提一句,在函数式语言中,递归函数不仅非常有用,还必不可少。

英文原文地址:https://blog.bitsrc.io/functional-programming-in-functions-composition-and-currying-3c765a50152e

作者:Fernando Doglio

更多编程相关知识,请访问:编程视频!!

The above is the detailed content of An in-depth analysis of higher-order functions, currying and combining functions in JavaScript. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:segmentfault.com. If there is any infringement, please contact admin@php.cn delete