函数式编程是一种编程方式,它和模板编程、面向过程编程、面向对象编程一样,是一种编程范式,同样编程的思维,也需要站在新的高度, 函数编程语言最重要的基础就是lambda演算。其实有很多语言,是专门的函数式编程语言,我也没有用过,C++11增加了lambda算子和std::function基础库,使得C++支持函数式编程。
函数式编程,最核心的理念,就是要有更范式的思维,去理解一段函数过程, 把它包装为一个函数, 并把函数当成“第一等公民”,让其其他数据类型一样可以作为其他函数的返回值,也可以作为其他函数的参数,可以动态编辑高阶函数和惰性函数。
函数是一段执行过程,在C++分为两类:函数指针和仿函数,而函数指针中的类成员函数比较特殊,一般而言会被转为仿函数在作为函数使用。仿函数是C++重载括号运算符的一些C++对象,我们称其为仿函数,最著名std::function, 他就是模板仿函数。
他们都有一个共同点,都是可以执行括号运算,其中lambda算子,是函数指针中比较特殊的一类。
lambda算子
Lambda算子的类型
lambda算子,一般我们会用std::function去接受,例如:
std::function<void(int)> func = [](int a){};
但是,一定注意的是,lambda算子也是有类型的,它不是std::function类型,这种写法,只不过是用lambda算子构造了一个std::function,但是它的类型,
我们手写不出来,我们可以选择,如下获取类型:
auto mycompare = [](int a, int b){
return a > b;
};
decltype(mycompare) b1; //编译会报错,因为decltype(mycompare)没有无参构造,必须写成
decltype(mycompare) b2 = mycompare;
b2(3, 1); // 正确
另外举一个实用的例子, std::map的第二个模板参数是需要小于比较仿函数类型,例如,我们写成:
struct MyCompare{
bool compare(int a, int b){
return a > b;
}
};
std::map<int, MyCompare> mapper; //这样写,之后,类内部,会默认使用MyCompare的无参构造,构造一个compare对象。
那么,我们也可以写成:
auto mycompare = [](int a, int b){
return a > b;
};
std::map<int, decltype(mycompare)> mapper(mycompare); // 这种写法必须传入mycompare
Lambda算子的闭包区间
C++的lambda算子,不同与其它的语言,定义匿名函数,自动闭包,C++11增加lambda算子,需要主动的闭包,就是 [] 所包含的变量。
只能包含左值引用和普通拷贝,且类成员变量,无法直接闭包,只能够传入this, 这就表示,无法直接拷贝类成员变量。
它的使用规则, 可以百度一下,这里不累述,需要说明的有一下几点。
闭包拷贝的东西,默认是不可修改的,不能在lambda算子的函数体内修改,如需修改,使用mutable修饰lambda算子。
例如:
int a = 0;
auto func = [a](){
++a; // 闭包作用域中a 是 const int 类型,无法修改
return a;
} ;
auto fun1 = [a]() mutable{
++a;
return a;
};
我们,可以理解lambda是一个类,而函数是一个const修饰的函数,闭包的变量是其成员,因此不能在const函数中修改成员变量。
当你直接写=号时,lambda算子并不会强行闭包所有东西,它只会闭包lambda算子你使用过的东西。
例如:
struct One{
char temp[1024];
};
One one;
auto func = [=](){}; // sizeof(func) == 1
auto func = [=](){ one;}; // sizeof(func) == 1024
同样,在lambda算子写 &, 也是同样的规则,但是好的习惯,是需要什么,主动写什么。
lambda算子拷贝的数据,多次调用lambda算子时,使用的是同一份数据。
例如:
int a = 0;
auto fun1 = [a]() mutable{
++a;
return a;
};
auto ret = fun1(); // ret == 1
auto ret1 = fun1(); // ret == 2
此时当前作用域的 a 仍然是 0.
使用引用的数据,需要注意lambda算子本身声明周期必须大于引用变量的声明周期。
这一点,非常的重要,lambda算子在执行时,所引用的变量,被释放,则会崩溃, 这一点需要非常的注意。
在闭包体内,如果多出返回,不能使用隐式转换,如果有隐式转换,则需要显示指定返回类型。
auto func = [](){
if(true)
return 1; //两处返回值,推导的返回类型不一致。
return 2.1;
};
如果,嫌麻烦,可以考虑,显示指定返回类型。
auto func = []()→double{
if (true)
return 1;
return 2.1;
};
lambda算子的等价替换
int a = 1;
double b = 3.0;
auto func = [a, &b] mutable (){
};
<==>
struct Func{
Func(int a, const double& b):a(a),b(b){
}
void operator()() const {
//由于a用mutable修饰,可以直接修改。
}
mutable int a;
double& b;
}func;
上述这两个定义,很大程度上,是完全一致的,不管是本身的大小,还是闭包的参数的使用方式, 因此,我们可以将lambda换成这种等价的方式去理解。
std::function
labmda算子是C++11新增的语法,std::function是C++11库中新增的一个模板类,这是模板仿函数类,从boost库中引入,由于C++11新增模板参数包,因此,std::function的实现并不难,有兴趣的可以直接实现一下。这里,也没必要对其进行细说,只不过我在filmora中看到有明显的使用错误,这里说明一下。
std::function的内存模型与std::any差不多,当构造传入的参数小于64个字节时,默认使用固定64进行强制替换,大于64个字节,申请堆存储,这就标准库的厉害之处,性能考虑无微不至。
其实我想表达的是,通常function我们都是用来执行括号运算符,并不会修改它本身的内容,因此在传递的过程,尽量使用常引用,不要赤裸裸写成普通的拷贝,这会带来多余性能和内存开销。
实现一个查询器
现在,我们假定有一个树形的结构如下所示:
struct Node{
Node* parent; //当前节点的父节点
std::vector<Node*> children; //当前节点的子节点集
std::map<std::string, std::string> attributes; //当前节点的属性列表,不同树属性的展示形式不一样,这里描述成这样。
}
现在我们建立一个这样的树
Node(age:10, color:yellow, name:li)
Node(age:20, color:blue,name:zhang)
Node(age:30, color:blue, name:zhang)
Node(age:25, color:yellow, name:liu)
Node(age:40, color:red, name:zhang)
Node(age:30, color:red, name:li)
Node(age:40, color:green, name:zhang)
Node(age:25, color:blue)
Node(age:35, color:blue)
Node(age:20, color:green)
Node(age:50, color:yellow,name:huang)
Node(age: 30, color:black)
Node(age:60, color:white, name:liu)
Node(age:80, color:white, name:ying)
Node(age:20, color:black, name:liu)
这是是一颗典型的树结构体,我们把他们装在进上诉定义的属性结构中。
现在我们需要做一个这样操作:
收集根节点下, name为zhang的子节点, 并收集这些节点的子节点中age大于25的节点。
正常的写法,先遍历根节点下,所有name为张的节点,然后遍历这些节点的子节点,判断age是否大于25.
代码如下所示:
Node* root;
std::vector<Node*> results;
for(Node* child : root→children){
if (child→attribute[“name”] == “zhang”){
for(Node* child1 : child→children){
if(child1→attribute(“age”) >25){
results.push_back(child1);
}
}
}
}
我们反观上诉有颜色部分的代码,抽象的理解,可以理解为一段输入为Node*的函数,因此我们定义如下:
using NodeVisitor = std::function<Node*>;
但是当你再仔细琢磨,红色模块包住的那一份代码,也就是绿色以及绿色包住的那些代码,同样也是一个
NodeVisitor,那这样就变得有意思了,我们再往里剥,你会发现仍然是一个NodeVisitor,也就是说,
外层的NodeVisitor依赖了里面的NodeVisitor,用函数的理解方式来说,就是一个NodeVisitor是另一个
NodeVisitor的参数,那么我们可以给出如下的定义:
using NodeFilter = std::function<Node*, const NodeVisitor&>;
那基于NodeFilter去理解,就又变味了,你会发现,单独的红色部分就是一个NodeFilter, 甚至你会发现
有颜色标注的模块,除了最内部的那份代码,其他都是NodeFilter,而且还有一个更深层的概念。
NodeFilter + NodeFilter = NodeFilter
NodeFilter + NodeVisitor = NodeVisitor
从外层往里层看,红色部分是NodeFilter, 绿色部分也是NodeFilter, 而这两部分加起来,还是缺少一个NodeVisitor,
因此仍然是一个NodeFilter。
而从里层往外层看,你会发现最里层是一个NodeVisitor, 粉色部分是一个NodeFilter, 两个加起来是NodeVisitor。
简单的说,我们得出一个如下的概念:
NodeVisitor = NodeFilter + NodeFilter + …. + NodeFilter + NodeVisitor
有了这些概念,我们就可以进行封装了,而上诉这种理解方式,就是高阶函数的理解思路。
Seeker的实现
这里为了方便理解,用的结构体,定义的过程进行拆分,我们按照上诉的理解,提供一个类动态去构造一个关于
Node* 的 NodeVisitor(访问器)。
第一步,补上上诉的两个定义
struct Seeker{
using NodeVisitor = std::function<Node*>;
using NodeFilter = std::function<Node*, const NodeVisitor&>;
};
第二步,定义成员变量
虽然我们的最终输出是一个Visitor,但是动态添加的过程,得到是Filter, 且Filter最后补上Visitor就是Visitor。
因此,我们的成员是一个Filter。
NodeFilter m_filter
第三步,增加过滤器
虽然,在前面已经讲到了 Filter + Filter = Filter, Filter + Visitor = Visitor, 理解起来也很简单,并且下面的代码也非常
的少,但理解难度是非常大,细细琢磨, 且最终的成品,代码更加的精简,这也是函数式编程的魅力。
Seeker& addFilter(const NodeFilter& filter){
//if (m_filter == nullptr){ //这玩意很讨厌,因此可以在构造函数强制指定其不为空
// m_filter = filter;
// return *this;
//}
//下面是重头戏,两个Filter怎么叠加,变成一个Filter
NodeFilter org_filter = m_filter;
m_filter = [org_filter, filter](Node* node, const NodeVisitor& visitor){
// 虽然过程是filter + filter == filter, 这里是假定有了一个visitor,
// 需要用新增加filter与假定visitor,构造原来Filter的Visitor
//因此这里需要用filter + visitor构造一个visitor。
NodeVisitor tempVisitor = std::bind(filter, _1, visitor);
org_filter(node, tempVisitor);
}
}
第四步,我们最终的定义的Filter,需要由一个Visitor终结。
void execute(const NodeVisitor& visitor){
m_filter(nullptr, visitor);
//这里第一个参数,需要是Node*, 这里传了一个nullptr进去,后续构造函数中,会补上原因哈。
}
第五步,收集过滤到的节点
这里的收集是真收集
std::vector<Node*> results(){
std::vector<Node*> rets;
execute([&rets](Node* node){
rets.push_back(node);
});
}
第六步, 增加上诉的几个过滤器
//属性过滤
Seeker& attribute(const QString& name, const QString& value){
return addFilter([](Node* node, const Visitor& visitor){
if (node→attribute(name) == value)
visitor(value);
});
};
//子节点过滤
Seeker& children(){
return addFilter([](Node* node, const Visitor& visitor){
for(Node* child : node→children){
visitor(value);
}
}
}
第六步,增加一个构造函数。
在上述中,有两个点,一个说到,需要构造函数,强制先指定m_filter不为空,
第二点,就是execute传入的Node*是nullptr。由于我们的Filter,需要两个参数,
一个是NodeVisitor, 一个是Node*, 我们可以考虑在execute中增加一个Node*的参数。
但是由于不好看,可以提前缓存到成员变量中,然后execute中将Node*传入进去。
Seeker(Node* node){
m_node = node;
//第一层,啥也不过滤,只为了弄一个非空的filter,防止后续的函数,做一些无聊的判断
m_filter = [](Node* node, const NodeVisitor& visitor){
visitor(node);
};
}
但是,你有没有发现,这里node, 其实不就是最后execute调用的filter需要传入的node吗?
因此我们写成.
Seeker(Node* node){
//第一层,啥也不过滤,只为了弄一个非空的filter,防止后续的函数,做一些无聊的判断
m_filter = [node](Node*, const NodeVisitor& visitor){
visitor(node);
};
}
于是,你惊喜的发现,压根我不用execute传入的node, 所以它传入nullptr也无所谓,也不用
成员去缓存node*。
整理一下代码
class Seeker{
public:
using Visitor = std::function<Node*>;
using Filter = std::function<Node*, const Visitor&>;
public:
Seeker(Node* node){
m_filter = [node](Node*, const Visitor& visitor){
visitor(node);
}
}
Seeker& add_filter(const Filter& filter){
Filter org_filter = m_filter;
m_filter = [org_filter, filter](Node* node, const Visitor& visitor){
org_filter(node, std::bind(filter, _1, visitor));
};
return *this;
}
Seeker& children(){
return add_filter([](Node* node, const Visitor& visitor){
for(Node* child : node→children){
visitor(child);
}
};
}
Seeker& attribute(const QString& name, const QString& value){
return add_filter[name, value](Node* node, const Visitor& visitor){
if (node→attributes[name] == value){
visitor(node);
}
}
}
void execute(const Visitor& visitor) const {
m_filter(nullptr, visitor);
}
std::vector<Node*> results() const{
std::vector<Node*> rets;
execute([&rets](Node* node){
rets.push_back(node);
});
return rets;
}
};
总结一下
那么基于这个Seeker的实现,我们再次对直接的查询过程进行实现:
auto results = Seeker(root).chilren().attribute(“name”, “zhang”).children().attribute(“age”,>25).results();
最后那个attribute不对,因为上述的实现,没有相应的接口哈,但是补充一个接口也简单。
我们总结一下,你会发现,使用Seeker有哪些好处呢?
- 它可以动态构建一个查询器,如果完善好接口,我们可以很快速的自定义一个查询过程。
- 有没有发现上诉的Seeker的使用,它非常贴合需求的语义?这样你写错代码的可能性几乎为0.
- 它没有存储任何的中间结果,效率是非常高的。
评论(0)