函数式编程是一种编程方式,它和模板编程、面向过程编程、面向对象编程一样,是一种编程范式,同样编程的思维,也需要站在新的高度, 函数编程语言最重要的基础就是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有哪些好处呢?

  1.   它可以动态构建一个查询器,如果完善好接口,我们可以很快速的自定义一个查询过程。
  2.   有没有发现上诉的Seeker的使用,它非常贴合需求的语义?这样你写错代码的可能性几乎为0.
  3.   它没有存储任何的中间结果,效率是非常高的。
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。