参考资料网页链接: https://juejin.cn/post/6878941871259779085 (函数式编程的实用场景)
https://github.com/xiaolingzang/python-skills/blob/master/%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B.md (函数式编程)
https://coolshell.cn/articles/10822.html(函数式编程)
本文整理了一些网络上的资料和介绍,同时依据初步理解编写了一个简单的对比demo,形成本篇。如有问题欢迎评论指正。
1 函数式编程
函数式编程中的「函数」指的是数学中的函数,即自变量的映射。函数的值取决于函数的参数的值,不依赖于其他状态,比如abs(x)函数计算x的绝对值,只要x不变,无论何时调用、调用次数,最终的值都是一样,即纯函数(pure function):
纯函数的条件:
- 函数内部不会依赖和影响外部的任何变量 (如果函数内使用了全局变量,就不能称为纯函数)
- 相同的输入,永远会得到相同的输出
纯函数的优点:
1. 可缓存
2. 可测试( 非纯函数代码是很难测试的)
3. 易于并发
先看一个非函数式的例子:
int cnt;
void increment(){
cnt++;
}
那么,函数式的应该怎么写呢?
int increment(int cnt){
return cnt+1;
}
你可能会觉得这个例子太普通了。是的,这个例子就是函数式编程的准则:不依赖于外部的数据,而且也不改变外部数据的值,而是返回一个新的值给你。
在函数式编程中,函数就是一个流水线,传递进去的数据(参数)就像是要加工的产品,我们可以通过用不同的加工设备加工出不一样的产品,但是这条流水线始终不会出现其他产线的任务产品。
函数式编程是一种声明式编程。(面向对象编程是一种命令式编程)
- 命令式编程是面向计算机硬件的抽象,一个命令式程序就是一个冯诺依曼机的指令序列
- 函数式编程是面向数学的抽象,将计算描述为一种表达式求值。一个函数式程序就是一个表达式。
因此:函数式编程就是属于声明式编程范式,这种范式会描述一系列的操作,但并不会暴露它们是如何实现的或是数据流如何传过它们。
函数式编程的几个技术:
- map & reduce函数式编程最常见的技术就是对一个集合做Map和Reduce操作。这比起传统的面向过程的写法来说,在代码上要更容易阅读(不需要使用一堆for、while循环来倒腾数据,而是使用更抽象的Map函数和Reduce函数)。
- pipeline※
这个技术的意思是把函数实例成一个一个的action,然后把一组action放到一个数组或是列表中组成一个action list,然后把数据传给这个action list,数据就像通过一个pipeline一样顺序地被各个函数所操作,最终得到我们想要的结果。 - recursing
递归最大的好处就简化代码,它可以把一个复杂的问题用很简单的代码描述出来。注意:递归的精髓是描述问题,而这正是函数式编程的精髓。 - currying
把一个函数的多个参数分解成多个函数, 然后把函数多层封装起来,每层函数都返回一个函数去接收下一个参数这样,可以简化函数的多个参数(减少函数的参数数目)。 - higher order function※
高阶函数:所谓高阶函数就是函数当参数,把传入的函数做一个封装,然后返回这个封装函数。现象上就是函数传进传出。
函数式编程的几个好处
- parallelization 并行
在并行环境下,各个线程之间不需要同步或互斥(变量都是内部的,不需要共享)。 - lazy evaluation 惰性求值
表达式不在它被绑定到变量之后就立即求值,而是在该值被取用的时候求值。 - determinism 确定性
输入是确定的,输出就是确定的。
可以看到函数式编程减少了变量的使用,也就减少了出Bug的可能,维护更加方便。可读性更高,代码更简洁。
我们可以很清楚地看到程序的主干,把代码逻辑封装成了函数后,我们就相当于给每个相对独立的程序逻辑取了个名字,于是代码成了自解释的。
但是,你会发现,在非函数式编程中,将功能封装成函数后,这些函数都会依赖于共享的变量来同步其状态。于是,我们在读代码的过程时,每当我们进入到函数里,一量读到访问了一个外部的变量,我们马上要去查看这个变量的上下文,然后还要在大脑里推演这个变量的状态, 我们才知道程序的真正逻辑。也就是说,这些函数间必需知道其它函数是怎么修改它们之间的共享变量的,所以,这些函数是有状态的。
我们知道,有状态并不是一件很好的事情,无论是对代码重用,还是对代码的并行来说,都是有副作用的。因此,我们要想个方法把这些状态搞掉,于是出现了我们的 Functional Programming 的编程范式。
下面附上一个小栗子 小例子来对比两种编程方法(函数式与非函数式),该代码是基于我的浅薄理解,有问题还请随时指出。
2 例子
2.1 非函数式例子
//
// Created by greg on 7/18/22.
//
/**
* 对data中的数据进行反序、*2、并按顺序输出3的倍数
*/
#include <iostream>
#include <stack>
int iArraySize = 10;
double data_arr[10] = {1,2,3,4,5,6,7,8,9,10};
double multiply = 2.0;
void process(double * _arr){
std::stack<double> stDouble;
for(int i = 0; i < iArraySize; i++)
{
stDouble.push(_arr[i]);
}
for (int i = 0; i < iArraySize;i++)
{
_arr[i] = stDouble.top()*multiply;
stDouble.pop();
}
for (int i = 0 ; i < iArraySize; i++)
{
if ((int)_arr[i]%3 == 0)
{
std::cout<<" "<<_arr[i];
}
}
}
int main() {
process(data_arr);
return 0;
}
process中处理了整个的操作过程,同时操作过程依赖了外部变量iArraySize 与 multiply。如果去掉注释的话,这简单的代码阅读起来还是要稍加理解才能明白操作过程。程序的输出如下:
如果此时,外部变量iArraySize 被“不小心”的改动为如12,则程序的输出如下:
此时,debug就要花费一些时间了,诸如整理逻辑、添加log等。
2.2 函数式例子
//
// Created by greg on 7/18/22.
//
/**
* 对data中的数据进行反序、*2、并输出3的倍数
*/
#include <iostream>
#include <stack>
#include <vector>
std::vector<double> data_arr = {1,2,3,4,5,6,7,8,9,10};
std::vector<double> printArr(std::vector<double> _arr){
// std::cout<<"in func_printArr"<<std::endl;
for (int i = 0 ; i < _arr.size(); i ++)
{
std::cout<<" "<<_arr[i];
}
std::cout<<std::endl;
// std::cout<<"end of func_printArr"<<std::endl;
return _arr;
};
std::vector<double> func_reverse(std::vector<double> _arr)
{
// std::cout<<"in func_reserve"<<std::endl;
std::stack<double> stDouble;
std::vector<double> stResult;
for (int i = 0 ; i < _arr.size(); i ++)
{
stDouble.push(_arr[i]);
}
for (int i = 0 ; i < _arr.size(); i ++)
{
stResult.push_back(stDouble.top()) ;
stDouble.pop();
}
// std::cout<<"end of func_reserve"<<std::endl;
// printArr(_arr);
return stResult;
}
std::vector<double> multiply2(std::vector<double> _arr){
// std::cout<<"in func_multiply2"<<std::endl;
for (int i = 0 ; i < _arr.size(); i ++)
{
_arr[i] = _arr[i]*2;
}
// std::cout<<"end of func_multiply2"<<std::endl;
// printArr(_arr);
return _arr;
}
std::vector<double> get3factor(std::vector<double> _arr)
{
// std::cout<<"in func_get3factor"<<std::endl;
std::vector<double> temp,re;
for (int i = 0 ; i < _arr.size(); i ++)
{
if ((int) _arr[i] % 3 == 0)
{
temp.push_back(_arr[i]);
}
}
for (int k = 0 ; k < temp.size() ; k ++)
{
re.push_back(temp[k]);
}
// printArr( re);
return re;
}
void process(std::vector<double> (*pGetFunctionError_Class[])(std::vector<double> ),std::vector<double> & _arr ,int _func_n){
std::vector<double> _p = _arr ;
for (int n = 0 ; n < _func_n; n++)
{
_p = pGetFunctionError_Class[n](_p);
}
}
int main() {
std::vector<double> (*funcs[4])(std::vector<double> ) = {func_reverse,multiply2,get3factor,printArr};
std::cout<<"the result of process is :"<<std::endl;
process(funcs,data_arr,4);
//test
std::cout<<"-------------------Test Part: ----------------------"<<std::endl;
std::cout<<"this is raw array:"<<std::endl;
printArr(data_arr);
std::cout<<"this is test of reserve:"<<std::endl;
printArr(func_reverse(data_arr));
std::cout<<"this is test of multiply2:"<<std::endl;
printArr(multiply2(data_arr));
std::cout<<"this is test of get3factor:"<<std::endl;
printArr(get3factor(data_arr));
std::cout<<"this is test of printArr:"<<std::endl;
printArr(data_arr);
return 0;
}
可以看到,整个过程分为了四步,在main中比较清晰的展示了:
std::vector<double> (*funcs[4])(std::vector<double> ) = {func_reverse,multiply2,get3factor,printArr};
- func_reverse: 反序
- multiply2: 乘法2
- get3factor: 获取3的倍数
- printArr: 打印字符串
整体的输出如下:(隐去Test输出部分)
如果此时由于某种“不小心”将multiply2中的*2变为了 *4,加入Test输出后如下:
可以看到,当出现问题后,可以很方便的分别调试不同函数部分的输出:反序、获取3倍数、打印数组都没有问题,看到multiply2的数组输出变为了4倍。因此快速定位了问题所在。这也印证了上文所述的函数式编程的优点:可测试及结果确定