欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  web前端

函数式编程的介绍和归纳总结(附代码)

程序员文章站 2022-03-08 10:22:27
...
最近在看函数式编程入门经典,自己总结归纳了一下,感兴趣或者有需要的可以看一下。

1.、什么是函数式编程

函数式编程主要是基于数学函数和它的思想,那么我们先复习一下数学中的函数即
y = f(x)
即函数 f(x) 以 x 为参数,以 y 为结果,x 和 y 可以是任意的数字,这其中包含了几个关键点:
1、函数必须总是接收一个参数
2、函数必须总是返回一个值
3、函数应该根据接收到的参数(例如 x),而不是外部环境运行
4、对于一个给定的 x,只会输出唯一的 y
让我们从一个例子中来理解

// 一个计税函数
var percentValue = 5;
var calculateTax = (value) => {return value/100 * (100 + percentValue)};

用数学中的函数来分析一下这个函数,首先看第三条,函数应该根据接收到的参数,而不是外部环境运行。这里的函数 calculateTax 依赖外部的 percentValue,因此,该函数在数学的意义上就不能称为函数,那么我们可以怎么将它改造成数学意义上的函数呢?很简单

var calculateTax = (value,percentValue) => {return value/100 * (100 + percentValue)};

现在这个函数就可以被称为一个真正的函数了。
现在让我们用简单的技术术语定义函数式编程:函数式编程是一种范式,仅依赖输入输出就可以完成自身逻辑的函数。这保证了当函数多次调用时依然可以返回相同的结果。函数不会改变任何外部环境的变量。

2 、引用透明性

根据函数的定义,我们可以得出结论:所有函数对于相同的输入输出都将返回相同的值。函数的这一属性被称为引用透明性
来举个例子

var identity = (i) => { return i };

我们定义了一个简单的函数,在函数的内部不依赖全局变量,它会简单的返回输入。现在假设它被应用于其他函数调用之间,如

sum(4,5) + identity(1);

根据引用透明性的定义,我们可以把它转换为

sum(4,5) + 1;

该过程被称为替换模型,因此你可以直接替换函数的结果(主要是因为该函数的逻辑不依赖与其他全局变量)。
由于函数对于给定的输入结果返回相同的值,实际上我们就可以缓存它了,比如我们有一个函数 “factorial”来计算阶乘。它接收一个参数以计算其阶乘,比如 5 的阶乘是 120。用户第二次输入 5 的阶乘时,由于引用透明性(对于相同输入返回相同结果)所以我们知道结果是 120,但是机器并不知道,我们需要让机器这个结果缓存下来以便以后调用直接返回结果,而不必再计算一遍。由此可以看出引用透明性和可缓存代码在函数式编程中的重要性。

3、函数式、声明式与抽象

函数式编程主张声明式编程和编写抽象的代码
什么是声明式编程
假设我们要打印出一个数组中的所有元素,我们可以采用如下方法

var array = [1,2,3];
for(let i = 0; i < array.length; i++){
    console.log(array[i])
}

在这段代码中,我们精确的告诉了代码该如何做。如:获取数组长度,循环数组,用索引获取每个元素。这就是命令式编程。命令式编程主张告诉编译器怎么做。
来看另一种方式

var arr = [1,2,3];
arr.forEach((ele) => { console.log(ele) })

上面这段代码我们移除了获取数组长度,循环数组,用索引获取数组元素等。我们只关心要做什么就行(即打印数组元素),获取数组长度,循环等都由机器帮我们做了,我们只需要关心做什么,而不是怎么做,这就是声明式编程
函数式编程主张以抽象的方式创建函数,这些函数能够在代码的其他地方被重用。

4、 纯函数

什么是纯函数?纯函数式对给定的输入返回相同输出的函数,例如

var double = (value) => value * 2;

上面的 double 就是一个纯函数,因为对于相同的输入总是返回相同的输出。纯函数遵循引用透明性,因此我们可以直接用 10 代替 double(5)。所以纯函数的最了不起的地方是什么?我们来看一下

1.4.1 纯函数产生可测试的代码
不纯的函数具有副作用,下面以之前的计税函数以例进行说明

var percentValue = 5;
var calculateTax = (value) => {return value/100 * (100 + percentValue)};

这个函数不是纯函数,主要因为它以来外部环境计算其逻辑,当外部环境改变时,它会影响结果。因此,纯函数的主要特征就是不依赖于任何外部变量,也不应该改变任何外部变量。如果改变了外部变量可能会引起其他函数的行为的改变,即产生副作用,这会使系统的行为变得难以预测。

1.4.2 合理的代码
我们应该通过函数的名字推理出函数的作用,比如 double 函数

var double = (value) => value * 2;

我们可以通过函数名轻易的推出这个函数会把给定的数值加倍,因此根据引用透明性,我们可以直接把 double(5) 替换成 10。还有一个例子,Math.max(3,4,5,6) 结果是什么?虽然我们只看到了函数的名字,但是我们很容易看出结果,我们看到实现了吗?并没有,为什么,就是因为 Math.max 是纯函数啊!!!

5、 并发代码

纯函数允许我们并发的执行代码,因为纯函数不会改变它的环境,这意味着我们根本不需要担心同步问题。当然,js 是单线程的,但是如果项目中使用了 webworker 来并发执行任务,该怎么办?或者有一段 Node 环境中的服务端代码需要并发的执行函数,又该怎么办呢?

// 非纯函数代码
let global = 'something'
let function1 = (input) => {
    // 处理 input
    // 改变 global
    global = "somethingElse"
}
let function2 = () => {
    if(global === "something"){
        // 业务逻辑
    }
}

如果我们需要并发的执行 function1 和 function2,假设 function1 在 function2 之前执行,就会改变 function2 的执行结果,所以并发执行这些代码就会造成不良的影响,现在把这些函数改为纯函数。

let function1 = (input,global) => {
    // 处理 input
    // 改变 global
    global = "somethingElse"
}
let function2 = (global) => {
   if(global === "something"){
        // 业务逻辑
    }
}

此处我们把 global 作为两个函数的参数,让它们变成纯函数,这样并发执行的时候就不会有问题了。

6、可缓存

既然纯函数对于给定的输入总能返回相同的输出,那么我们就能缓存函数的输出,例如

var doubleCache = (value) => {
    const cache = {};
    return function(value){
        if(!cache[value]){
            cache[value] = value * 2
            console.log('first time')
        }
        return cache[value];
    }
}
var double = doubleCache();
double(2) // first time,4
double(2) // 4
// 或者直接使用立即执行函数
var double = ((value) => {
    const cache = {};
    return function(value){
        if(!cache[value]){
            cache[value] = value * 2
            console.log('first time')
        }
        return cache[value];
    }
})()
double(2) // first time,4
double(2) // 4

这个函数中,假设我们第一次输入 5,cache 中并没有,于是执行代码,由于闭包的存在,cache[5] = 10,第二次我们调用的时候,cache[5] 存在,所以直接 return 10,看到了吗?这就是纯函数的魅力!!!别忘记这是因为纯函数的引用透明性。

7、 管道与组合

纯函数应该被设计为一次只做一件事,并且根据函数名就知道它所做的事情。
比如 linux 系统下有很多日常任务的命令,如 cat 用于打印文件内容,grep 用于搜索文件,wc 用于计算行数,这些命令一次只解决一个问题,但是我们可以用管道或组合来完成复杂的任务。假设我们需要在一个文件中找到一个特定的名称并统计它的出现次数,在命令行要输入如下指令
cat jsBook | grep -i “composing” | wc
上面的命令通过组合多个函数解决了我们的问题。组合不是 linux 命令独有的,它们是函数式编程范式的核心。
我们把它们称为函数式组合。来看一个 compose 函数的例子

var add1 = (value) =>{ return value+1 };
var double = (value) => {return value*2 };
var compose = (a,b) => {
    return (c) => {
       return a(b(c));
    }
}
var doubleAndAdd1 = compose(add1,double);
doubleAndAdd1(5) // 打印 5 * 2 + 1 = 11

compose 函数返回一个函数,将 b 的结果作为 a 的参数,这里就是将 double 的结果作为 add1 的参数,来实现了函数的组合。

8、 纯函数是数学函数

还记得我们之前的缓存函数吗,假设我们多次调用 double 对象,那么 cache 中就会变成这样

{
    1: 2,
    2: 4,
    3: 6,
    4: 8,
    5: 10
}

假设我们设置 double 的输入范围限制为 1 - 5,而且我们已经为这个范围建立的 cache 对象,因此只要参照 cache 就能根据指定输入返回指定输出。也就是一一对应的关系。
那么数学函数的定义是什么呢?
在数学中,函数是一种输入集合和可允许的输出集合之间的关系,具有如下属性:每个输入都精确地关联一个输出。函数的输入称为参数,输出称为值。对于一个给定的函数,所有被允许的输入集合称为该函数的定义域,而被允许的输出集合称为值域。

上面的定义和纯函数完全一致,例如在 double 中,你能找到定义域和值域吗?当然可以!通过这个例子,可以很容易看到数学函数的思想被借鉴到函数式范式的世界

9、 我们要做什么?

我们将通过学习,构建出一个 ES6-Functional 的函数式库,通过构建的过程,我们将理解如何使用 JavaScript 函数,以及如何在日常工作中应用函数式编程。

10、小结

这一节我们只是简单的介绍了函数式编程的概念,以及什么是纯函数,其中最重要的就是引用透明性。然后研究了几个短小的例子,通过例子来加深对函数式编程的理解。接下来我们将一步一步深入了解函数式编程。

以上就是函数式编程的介绍和归纳总结(附代码)的详细内容,更多请关注其它相关文章!

相关标签: 函数式编程