C++函数式编程

由于我自己也是在学习的过程中,并没有形成具体的函数式思维,因此可能记录的学习过程比较乱

标准库的基本使用

std::pair

std::pair是标准库中所有键值数据(key-value data)的默认返回类型,是快速使用标准库的第一把钥匙

重载输出流

这段代码高亮有点问题,我本地md里正常,修不好……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<iostream>
#include<string>
#include<utility>
#include<functional>

using namespace std;

template<class T1, class T2>
ostream& operator<<(ostream& out, const pair<T1, T2>& _) {
return out << "(" << _.first << "," << _.second << ")" << endl;
}

int main(int argc, char**argv) {
std::string names[3] = { "lambda","C++","pwn" };
int scores[3] = { 3,4,5 };
std::pair <std::string, int> p0 = std::make_pair(names[1], scores[1]);
//p0 = (C++,4)
auto p1 = std::make_pair(names[2], scores[2]);
//p1 = (pwn,5)
auto p2 = std::make_pair(names[1], std::ref(scores[1]));
scores[1] += 10;
//p0 不变
//p2 = (C++,14) std::ref()
cout << p0 << p1 << p2;
return 0;
}

std::tuple

更一般地,元组(tuple)可以把多个不同数据类型的实例绑定到一个实例上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include<iostream>
#include<string>
#include<tuple>
#include<functional>

using namespace std;

int main(int argc, char**argv) {
std::string names[3] = { "C++","lambda","reverse" };
char ranks[3] = { 'A','B','C' };
int scores[3] = { 5,6,7 };

auto student0 = std::make_tuple(names[0], ranks[0], scores[0]);
std::cout << "student0 name:" << std::get<0>(student0)
<< ", rank:" << std::get<1>(student0)
<< ", score:" << std::get<2>(student0) << std::endl;
//student0 name:C++, rank:A, score:5

std::string s;
char c;
int i;
auto student1 = std::make_tuple(names[1], ranks[1], scores[1]);
std::tie(s, c, i) = student0;
std::cout << "student1 name:" << s
<< ", rank:" << c
<< ", score:" << i << std::endl;
//student1 name:C++, rank:A, score:5

/*
auto student2 = std::tie(names[2],ranks[2],scores[2]);
auto[_1, _2, _3] = student2; // C++17 structured binding
std::cout << "student2 name:" << _1
<< ", rank:" << _2
<< ", score:" << _3 << std::endl;

g++-7 main.cpp -std=c++17
输出:student2 name:reverse, rank:C, score:7
*/

return 0;
}

tuple的组装和拆包任务分别由两个函数来担当

std::make_tuple创建一个元组,std::get获取确定位置的数据

注意到,std::tie既可以组装元组,也可以对一个元组进行拆包

1
std::tie(std::ignore, std::ignore, xxx) = student0;

std::ignore 可以作为解包时的占位符

auto[a,b,c]=xxx 是复制一份数据

std::tie()默认会创建原变量的引用

std::vector

基本的使用就不提了,后文会经常用到,考虑一下vector扩充内存的细节

当反复push_back()一个元素时,一旦达到了上限,vector会把可用空间大小扩充为之前的k倍,一般为2或1.5,具体由编译器决定

lambda函数

基本使用

举个两个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>
#include<vector>

using namespace std;

int main(int argc, char**argv) {
auto addition = [](int _1, int _2)->int { return _1 + _2; };
vector<int>a = { 1,2,3,4,5 };
vector<int>b = { 6,7,8,9,10 };
for (int i = 0, sz = a.size(); i < sz; i++) {
a[i] = addition(a[i], b[i]);
}
for (auto e : a)cout << e << endl;

return 0;
}

要注意的是,addition并不是lambda函数的名字,它只是被附身了而已…lambda本身是没有名字的,因此不能简单地实现递归

参数列表中的中括号,名为捕捉列表,用于捕捉当前lambda所在环境的变量

参数列表之后有一个箭头,箭头后跟着返回值的类型

最后,与函数声明完之后直接用大括号收尾不同,在这里可以暂时把lambda函数看做一个特殊的变量,所以最后用分号收尾

auto a_alias = [capture](parameters)->return_type{...};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<iostream>
#include<vector>

using namespace std;

int main(int argc, char**argv) {
int j = 1;
auto by_val_lambda = [=] { return j + 1; };
auto by_ref_lambda = [&] { return j + 1; };
auto print = [=] {
cout << "print by val lambda: " << by_val_lambda() << endl;
cout << "print by ref lambda: " << by_ref_lambda() << endl;
};
print();
j += 10;
print();

return 0;
}

结果如下

1
2
3
4
print by val lambda: 2
print by ref lambda: 2
print by val lambda: 2
print by ref lambda: 12 //引用传递

[=]表示按照值传递的方法捕捉父作用域的所有变量

[&]表示按照引用传递的方法捕捉父作用域的所有变量

如果想要捕捉特定的变量,可以直接写[var]或[&var]

甚至可以捕捉this指针,如果父作用域存在的话

lambda函数的出现减少了对命名空间的污染,也可以理解为增加了一种简单的局部函数的实现

可以直接把函数写在main函数里,用函数指针来管理

lambda递归

计算阶乘

1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream>
#include<functional>
#include<vector>

using namespace std;

int main(int argc, char**argv) {
std::function<int(int)>fact;
fact = [&fact](int x)->int { return x <= 1 ? x : x * fact(x - 1); };
cout << fact(5) << endl;
return 0;
}

lambda实现递归时,可以提前声明fact,然后再用[&fact]捕获

当右值赋给fact时,也就是fact函数指针被lambda”附身”,fact的值改变,那么fact的引用也对应改变

以下为照抄部分:链接

准确地讲,上面根本不是lambda函数的递归调用,只是一般函数的递归而已

想要实现lambda函数的递归调用,必须首先对Y-组合子有一定的了解

简单地说,虽然lambda没有名字,但我们可以把它作为一个参数传递给更上层的函数调用

熟悉函数式编程的话会想起不动点组合子,也就是构造一个函数,返回它的不动点

1
2
fix :: (a -> a) -> a
fix f = let {x = f x} in x

抄不动了,我也不熟悉函数式啊…..但是直接用C++还是会写的…

函数式编程工具箱

把函数看作一等公民

传统的 C++ 把承载数据的变量作为一等公民,这些变量享有以下权利:

1、可以承载数据,也可以存在于更高级的数据结构中;

2、可以作为右值参与计算、函数调用;

3、可以作为左值参与赋值操作。

那么,如果把函数也当做一等公民,那么,函数也应当享有以下的权利:

1、函数就是数据,一个函数变量可以承载函数,也可以存在于更高级的数据结构中;

2、可以作为右值参与计算,或参与更高级的函数调用;

3、可以作为左值参与赋值操作。

这里给一个简单计算器的例子来理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <functional>
#include <cmath>
#include <map>

using namespace std;

int main(int argc, char**argv) {
std::map<char, std::function<double(double, double)> > host;
host['+'] = [](double a, double b)->double { return a + b; };
host['-'] = [](double a, double b)->double { return a - b; };
host['*'] = [](double a, double b)->double { return a * b; };
host['/'] = [](double a, double b)->double { return a / b; };
host['^'] = [](double a, double b)->double { return pow(a, b); };
cout << "1.1 + 2.2= " << host['+'](1.1, 2.2) << endl; // => 3.3
cout << "1.1 - 2.2= " << host['-'](1.1, 2.2) << endl; // => -1.1
cout << "1.1 * 2.2= " << host['*'](1.1, 2.2) << endl; // => 2.42
cout << "1.1 / 2.2= " << host['/'](1.1, 2.2) << endl; // => 0.5
cout << "1.1 ^ 2.2= " << host['^'](1.1, 2.2) << endl; // => 1.23329

return 0;
}

这里我们使用了标准库提供的std::map来存储核心匹配关系,即一个运算符对应一个运算关系

这时map存储的数据不再是数字,而是一个具体的函数,一种具体的运算方法,这就是刚刚提到的公民权利的第一条和第三条(‘+’作为一个函数载体,作为左值)

而第二条:作为右值参与计算或参与更高级的函数调用,根据以下的三个工具来理解

基本工具

mapfilterfold分别表示对集合而言的三种操作:映射、筛选、折叠

映射

映射操作:对一个列表vec内的所有元素依照某种运算法则f进行运算,从而得到一个全新的列表[f(x) for x in vec],这确实是一个基于列表的循环操作

提到函数式编程时,遵循两个原则:

1、DRY(Don’t Repeat Yourself),一个组件只有一次实现,绝不特化过多的实现

2、重视逻辑推演而非执行过程,侧重如何通过逻辑上的组合、推演,来完备的定义问题的解,执行过程交给计算机即可

STL中提供了两个工具:std::for_eachstd::transform,后者支持更好甚至支持两个列表同时计算

for_each写法比for+auto复杂,关键是还保证了顺序执行,相比for优势全无,不过这一点也可以使它应用于一个非纯函数的工具

纯函数是指没有副作用、没有后继状态变化,对相同输入总会反馈相同结果的一类特殊函数

某种意义上说,函数式编程(尽量)要求使用纯函数从而方便测试(反馈唯一)、维护(没有副作用)、自动并行化(没有后继的状态变化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<iostream>
#include<vector>
#include<algorithm>

using namespace std;

int main(int argc, char**argv) {
std::vector<int> a = { 1,2,3,4 };
int sum = 0, idx = 1, sum2 = 0;
auto add_to_sum = [&sum](int _) {sum += _; };
auto add_to_sum_with_idx = [&sum2, &idx](int _) {sum2 += (_ * idx++); };

for (auto e : a) cout << " " << e;
cout << endl;
for_each(a.begin(), a.end(), add_to_sum);
cout << sum << endl; // 1+2+3+4
for_each(a.begin(), a.end(), add_to_sum_with_idx);
cout << sum2 << endl; // 1 * 1 + 2 * 2 + 3 * 3 + 4 * 4
//可以考虑差比数列求和 AP * GP,这里只是个简单的非纯函数例子

return 0;
}

回到transform,它不保证按序执行,它要求函数不改变迭代器(比如delete)或者修改范围内部元素(比如把下一个元素的值+1)

同时transform要求有返回值,每执行一次都会把结果存储在一个新的列表中

transform允许同时接受两个列表,但是此时要求后者的长度必须大于等于前者的长度,否则会产生未定义行为(Undefined Behavior)

实际上transform也可以实现就地操作,只要把输出的迭代器位置变换成自己的begin()就可以了

举个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>
#include<vector>
#include<algorithm>

using namespace std;

int main(int argc, char**argv) {
std::vector<int>a = { 1,2,3,4,5,6 };
std::vector<int>b;
std::transform(a.begin(), a.end(), std::back_inserter(b),
[](int _)->int { return _ + 3; });
//遍历a,每个数据+3后插入到vector b中

return 0;
}

筛选

映射是所有操作的根基,因为所有操作都可以用映射进行解释,筛选和折叠也不例外

筛选:对一个列表内的所有元素应用一个谓词(predicate,谓词特指判断事物返回True或False的函数),然后取出所有结果为True的元素,简单地说就是for循环 + if-else判断

STL提供了copy_ifremove_if来实现筛选,给出一个实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<iostream>
#include<vector>
#include<algorithm>

using namespace std;

int main(int argc, char**argv) {
auto print_value = [](auto v) {for (auto e : v)cout << " " << int(e); cout << endl; };
#define DISPLAY_VALUE(v)do{cout<<#v":";print_value(v);}while(0);
std::vector<int> a = { 1,2,3,4,5,6,7,8,9,0 };
std::vector<int> b;
std::copy_if(a.begin(), a.end(), std::back_inserter(b),
[](int _) {return _ % 2; });
DISPLAY_VALUE(a);
DISPLAY_VALUE(b);

a.erase(std::remove_if(a.begin(), a.end(), [](int _) { return _ % 2; }), a.end());
DISPLAY_VALUE(a);
#undef DISPLAY_VALUE
// 输出结果
// a: 1 2 3 4 5 6 7 8 9 0
// b: 1 3 5 7 9
// a: 2 4 6 8 0

return 0;
}

需要注意的是,copy_if需要额外开一块空间出来,remove_if则返回了需要删掉的元素的迭代器,然后再通过erase函数彻底删除元素

transform类似,这里的谓词函数同样要求不得修改原始数据

折叠

折叠:对一个列表和一个初始值应用一个二元函数,然后依次按某一特定方向将整个列表折叠起来

看完代码就能理解这个描述了

STL中提供了accumulateinner_product,两者都是左折叠函数,分别是执行求和、计算向量内积的函数

后者实际上是先对两个列表应用一个二元运算(默认乘法),把结果存入一个列表中,再对这个列表折叠(默认求和)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include<iostream>
#include<vector>
#include<algorithm>
#include<numeric>

using namespace std;

int main(int argc, char**argv) {
auto print_value = [](auto v) {for (auto e : v)cout << " " << int(e); cout << endl; };
#define DISPLAY_VALUE(v)do{cout<<#v":";print_value(v);}while(0);
std::vector<int> a = { 1,2,3,4,5,6,7,8,9,10 };
int sum = std::accumulate(a.begin(), a.end(), 0,
[](int acc, int _) { return acc + _; });
cout << sum << ", ";
DISPLAY_VALUE(a);
// 55, a: 1 2 3 4 5 6 7 8 9 10

int prod = std::accumulate(a.begin(), a.end(), 1,
[](int acc, int _) { return acc * _; });
cout << prod << ", ";
DISPLAY_VALUE(a);
// 3628800, a: 1 2 3 4 5 6 7 8 9 10

//反转vector
std::vector<int> a_re = std::accumulate(a.begin(), a.end(), std::vector<int>(),
[](std::vector<int> acc, int _)->std::vector<int> {
std::vector<int> __; __.push_back(_);
std::copy(acc.begin(), acc.end(), std::back_inserter(__));
return __;
});
DISPLAY_VALUE(a_re);
// a_re: 10 9 8 7 6 5 4 3 2 1

#undef DISPLAY_VALUE

return 0;
}

简单地说,应用的二元函数的第一个参数是累加值,第二个参数是列表中取出的值,返回值的类型应与初值和累加值的类型相同(熟练以后可以用auto代替)

最后一个反转vector略有一点难度,如果看不懂可以输出中间结果看看

考虑一个问题,由于C++依旧是基于面向过程面向对象的语言,所以在函数式编程上的优化可能没有那么专业,而且C++会按语句依次执行,如果大量堆叠transform会不会造成性能下降呢?

这就是之后将要引入的内容:是否引入简单的lazy求值,以及简单的柯里化

题外话:这里我突然想到了一些东西,当看到这句话期末考完可能得等,它是很不符合语法的语句,但是我们可以轻易得知语句表达的含义。我猜测,眼睛读取信息时并行的读入这8个汉字,然后由大脑根据词组进行全排列,比如这8个字分成了四个词组,然后匹配最佳语义(又能扯到SFINAE了吗…)

函数的部分应用

在处理柯里化之前,我们先思考这个问题:如何对一个函数进行部分应用

举个例子,比如最常见的除法a/b,能否只传递其中的一个参数给这个除法运算,从而得到一个接收另一个参数的函数?话很绕口,但是代码不绕口

1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream>

using namespace std;

int main(int argc, char**argv) {
auto div = [](int a, int b)->int { return a / b; };
auto div3 = [div](int a)->int {return div(a, 3); };
auto ten_div = [div](int b)->int {return div(10, b); };
cout << div(10, 3) << " " << div3(10) << " " << ten_div(3) << endl;
// 输出都是3
return 0;
}

这只是个简陋的例子,看不出来这有什么用……

再给出使用std::bind()的更优雅(繁琐)的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <functional>

using std::cout;
using std::endl;

void f(int x, int y, const int &z){
cout << "=> f(" << x << ", " << y << ", " << z << ")" << endl;
}

int g(int x, int y, int z) { return x + y * z; }


int main(int argc,char**argv){
using namespace std::placeholders;
int n = 5;
//std::placeholders::_1,_2... 占位符
auto fyx5 = std::bind(f, _2, _1, n);
fyx5(1, 2); // => f(2, 1, 5)
auto f5yy = std::bind(f, n, std::ref(n), std::cref(n));
f5yy(); // => f(5, 5, 5)
n += 2; f5yy(); // => f(5, 7, 7)

auto gxxx = std::bind(g, _1, _1, _1);
cout << "5 + 5 * 5 = " << gxxx(5) << endl; // => 5 + 5 * 5= 30
//这里gxxx(5,1,2,3,4,5)的结果也是一样的,因为占位符都用的第一个参数

auto gx45 = std::bind(std::bind(g, _1, _2, 5), _1, 4);
//内层std::bind返回一个函数对象,绑定了第三个参数;外层则绑定第二个参数
cout << "3 + 4 * 5 = " << gx45(3) << endl; // => 3 + 4 * 5= 23

return 0;
}

这个例子其实并没有做到部分应用,只是使用了占位符std::placeholders::_1,_2,...来实现效果

std::bind()的参数默认是值传递,若需要引用传递,必须使用std::ref()std::cref()

更好的写法,一个简单的柯里化尝试

1
2
3
4
5
6
7
auto c_xor = [](char _1)->auto{
return[_1](char _2)->auto{
return [_1, _2](char _3) {
return _1 ^ _2 ^ _3;
};
};
};

可以这样调用 char c = c_xor('a')('b')('c');

柯里化

我们不想为了柯里化而柯里化,把每一个函数的声明都写的十分冗长;也不想用std::bind每次都把所有参数写出来,形式不够简洁

两个方案

1、需要额外手动维护一个记录参数的容器

2、利用变长模板进行函数包装

把元组应用到函数上

原本函数调用是由编译器来托管所有参数进栈出栈,现在只能一次来一个参数,自然会想到用一个容器来托管所有参数,一旦调用,就把这些参数都喂给原始函数

当然是选择tuple了…但是如果能直接把tuple内的参数直接喂给函数就好了

最近的C++17标准中提供了apply函数来完成这一操作

这里我们自己写一份

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include<iostream>
#include<tuple>
#include<utility>
#include<functional>

using namespace std;

template<class Func, class Tuple, std::size_t... I>
constexpr auto tuple_apply_impl(Func&& f, Tuple&& t,
std::index_sequence<I...>) {
return std::invoke(std::forward<Func>(f), std::get<I>(std::forward<Tuple>(t))...);
// 如果f的范围限定为函数对象
// 那么f(args...)和std::invoke(f,args...)是完全等价的
}

template<class Func, class Tuple>
constexpr auto tuple_apply(Func&& f, Tuple&& t) {
return tuple_apply_impl(
std::forward<Func>(f), std::forward<Tuple>(t),
std::make_index_sequence<std::tuple_size<std::remove_reference_t<Tuple>>::value>{}
// 借助std::make_index_sequence<>在编译时把下标序列构造出来
// 然后应用到std::get<I>上
);
}


int main(int argc, char**argv) {
auto add = [](int a, int b)->int { return a + b; };
auto add_3_nums = [](int a, int b, int c)->int { return a + b + c; };
cout << std::apply(add, std::make_pair(1, 2)) << endl;
cout << std::apply(add_3_nums,std::make_tuple(1,2,3))<< endl;

cout << tuple_apply(add,std::make_pair(1,2)) << endl;
// 这里编译后就会执行 std::invoke(add,std::get<0>(t),std::get<1>(t))

cout << tuple_apply(add_3_nums,std::make_tuple(1,2,3)) << endl;
// 3 6 3 6
return 0;
}

// g++-7 main.cpp -std=c++17 编译成功,vs的C++版本不够

function_traits

如何判断这个函数需要多少个参数呢?

这里我们需要写一个类型判断器,把函数解构,获取一个函数的必要信息:返回类型(return type)是什么,参数(arity)需要多少个,每一个参数的类型(argument type)是什么

我们把这些东西包装到一个function_traits类型中

并且把函数的类型规范到形如std::function<Return(Args...)>

当然为了确保我们的类型是真实可靠的,借助于C++非常严格的类型检验功能std::is_same<U,V>::value,同时借助typeid().name()把这个类型具体是什么输出出来

如果使用的是GCC,typeid().name()会得到编译器装饰过的类型别名(decorated name),难以阅读

这里使用GCC自带的工具c++filt或者__cxa_demagle来帮助把类型名替换为代码中的原始名称

这里使用的是后者,并且用函数realname()帮助我们把类型直接转成正常形式

学习于 知乎专栏,遗憾的是我这里g++-7跑不了这个代码,在宏相关的地方有报错,但是我不会debug…

贴一下function_traits.hpp的代码,后面可以正常使用,我并没有尝试理解这段代码…

后面的两段柯里化代码都需要借助这份hpp头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// filename: function_traits.hpp
#ifndef __FUNCTION_TRAITS_HPP__
#define __FUNCTION_TRAITS_HPP__
namespace __utils{

template <class F> struct function_traits;

// normal function, static member function
template <class Return, class...Args>
struct function_traits<Return(Args...)>
{
using func_type = std::function<Return(Args...)>;
using return_type = Return;
static constexpr std::size_t arity = sizeof...(Args);
template <std::size_t I> struct argument
{
static_assert(I < arity,
"error: invalid index of this function's parameter.");
using type = typename
std::tuple_element<I, std::tuple<Args...>>::type;
};
};

// function pointer, &f
template <class Return, class...Args>
struct function_traits<Return(*)(Args...)>
: public function_traits<Return(Args...)> {};

// std::function
template <class Return, class...Args>
struct function_traits<std::function<Return(Args...)>>
: public function_traits<Return(Args...)> {};

// functor, callable object, struct with overloaded operator()
template <typename Functor>
struct function_traits
: public function_traits<decltype(&Functor::operator())> {};

// lambda expression, const member function pointer
template <class Class, class Return, class...Args>
struct function_traits<Return(Class::*)(Args...) const>
: public function_traits<Return(Args...)> {};

// member function pointer
template <class Class, class Return, class...Args>
struct function_traits<Return(Class::*)(Args...)>
: public function_traits<Return(Args...)> {};

// member object pointer
template <class Class, class Return>
struct function_traits<Return(Class::*)> : public function_traits<Return()> {};

// clear reference F&, F&&
template <class F> struct function_traits<F&> : public function_traits<F> {};
template <class F> struct function_traits<F&&> : public function_traits<F> {};

// make_func(), return a std::function<> object from function-like object
template <class Func> auto make_func(Func && f)
{
typename function_traits<Func>::func_type _f = std::forward<decltype(f)>(f);
return std::forward<decltype(_f)>(_f);
}

// arity(), return numbers of parameters
template <class Func> inline constexpr std::size_t arity(Func &&)
{
return function_traits<std::remove_reference_t<Func>>::arity;
}

// argument_type<Func, I>::type, return I-th parameter's type, I start from 0
template <class Func, std::size_t I> struct argument_type
{
using type = typename function_traits<Func>::template argument<I>::type;
};

} // namespace __utils
#endif // __FUNCTION_TRAITS_HPP__

实现柯里化包装器

我们的要求依旧和之前一样,既要保持函数不必每次重写,也要保证形式上的简洁性

最初已经提到了这两个方案,顺着这个思路往下走

1、需要额外手动维护一个记录参数的容器

2、利用变长模板进行函数包装

带有一定lazy程度的currying

既然已经知道如何把元组应用到函数上了,那么只需要手动维护这个元组就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <iostream>
#include <tuple>
#include <functional>
#include <cassert>

using std::cout;
using std::endl;

#include "function_traits.hpp"

template <class Func, class Tuple, std::size_t... I>
constexpr auto tuple_apply_impl(const Func &f, Tuple && t, std::index_sequence<I...>)
{
return f(std::get<I>(std::forward<Tuple>(t))...);
}

template <class Func, class Tuple>
constexpr auto tuple_apply(Func && f, Tuple && t)
{
return tuple_apply_impl( std::forward<Func>(f), std::forward<Tuple>(t),
std::make_index_sequence<std::tuple_size<std::remove_reference_t<Tuple>>::value>{});
}

template <class Func, class Tuple = std::tuple<> >
class curried_functor
{
private:
Func f_;
Tuple t_;
std::size_t argc_;
using final_ret_type = typename __utils::function_traits<Func>::return_type;
public:
curried_functor(Func && f)
: f_(std::forward<Func>(f)), argc_(__utils::arity(f_)) {}
curried_functor(const Func &f, const Tuple &t)
: f_(f), argc_(__utils::arity(f_)), t_(t) {}
template <class... Args>
auto operator()(Args&&... args) const -> final_ret_type
{
std::size_t tuple_len = std::tuple_size<Tuple>::value;
std::size_t args_len = sizeof...(args);
assert(tuple_len + args_len == argc_); // test if parameters don't match!
auto argv = std::tuple_cat(t_, std::make_tuple(std::forward<Args>(args)...));
return tuple_apply(f_, argv);
}
template <class Arg> auto curry(Arg && arg) const
{
std::size_t tuple_len = std::tuple_size<Tuple>::value;
std::size_t arg_len = 1;
assert(tuple_len + arg_len <= argc_); // test if too much curry!
auto argv = std::tuple_cat(t_, std::make_tuple(std::forward<Arg>(arg)));
return curried_functor<Func, decltype(argv)>(f_, argv);
}
};

template <class Func> auto curry(Func && f)
{
auto _f = __utils::make_func(f);
return curried_functor<decltype(_f)>(std::forward<decltype(_f)>(_f));
}

#define DISPLAY(expr) do { cout << #expr"= " << expr << endl; } while (0);

int main()
{
cout << "-------- 4 - 5= ? --------\n";
auto sub = [](int a, int b)->int{ return a - b; };
auto curried_sub = curry(sub);
auto curried_sub_4 = curried_sub.curry(4);
DISPLAY(curried_sub(4, 5)); // => -1
DISPLAY(curried_sub_4(5)); // => -1
DISPLAY(curried_sub.curry(4).curry(5)()); // => -1

cout << "-------- 2 * 3 * 4= ? --------\n";
auto mul3 = curry([](int a, int b, int c)->int{ return a * b * c; });
DISPLAY(mul3(2, 3, 4)); // => 24
DISPLAY(mul3.curry(2)(3, 4)); // => 24
DISPLAY(mul3.curry(2).curry(3)(4)); // => 24
DISPLAY(mul3.curry(2).curry(3).curry(4)()); // => 24
return 0;
}

代码中的柯里化是具备了一定的lazy功能的,只有外部显式调用operator()的时候才会进行计算

但是这样并没有完全lazy计算,因为传参时,我们传递的值,而不是参数的计算定义或计算路径

仍然是看不懂,可能这就是库的作用吧

知乎专栏

把函数包装托管给编译器

利用模板特化,把函数包装托管给编译器

但是这里的柯里化就不再是延时求值了,而是每一次柯里化都返回一个真实的函数对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <iostream>
#include <tuple>
#include <functional>
#include <cassert>

using std::cout;
using std::endl;

#include "function_traits.hpp"

//这两个函数规定了一元函数和零元函数的柯里化规则
//在有了这个终止递归条件之后,我们就可以用变长模板来定义更一般的柯里化规则
template <class Return>
auto curry_impl(std::function<Return()> && f){
return std::forward<decltype(f)>(f);
}

template <class Return, class Arg>
auto curry_impl(std::function<Return(Arg)> && f){
return std::forward<decltype(f)>(f);
}



//在这里,所有的参数和函数对象都用右值引用进行转发,保证效率和值的不变性
//然后只需要把第一个参数写到调用列表就行了
//最后,[通过`function_traits::func_type`把函数规整到`std::function`对象]这一功能
//被我们封装到了curry()函数中
template <class Return, class Arg, class... Args>
auto curry_impl(std::function<Return(Arg, Args...)> && f){
return[f = std::forward<decltype(f)>(f)](Arg arg){
std::function<Return(Args...)> rest
= [f = std::forward<decltype(f)>(f), arg = std::forward<Arg>(arg)]
(Args... args)->Return { return f(arg, args...); };
return curry_impl(std::forward<decltype(rest)>(rest));
};
}


template <class Func> auto curry(Func && f){
auto _f = __utils::make_func(f);
return curry_impl(std::forward<decltype(_f)>(_f));
}


#define DISPLAY(expr) do { cout << #expr"= " << expr << endl; } while (0);

int main(int argc,char**argv){
cout << "-------- subtraction --------\n";
auto sub = [](int a, int b)->int { return a - b; };
auto curried_sub = curry(sub);
//使用时,可以保持原函数的写法不做改变,只需要外部对其进行一次curry()修饰即可
auto curried_sub_4 = curried_sub(4);
DISPLAY(curried_sub_4(5)); // => -1
DISPLAY(curried_sub(4)(5)); // => -1

cout << "-------- multiplication --------\n";
auto mul3 = curry([](int a, int b, int c)->int { return a * b * c; });
//curry()修饰
DISPLAY(mul3(2)(3)(4)); // => 24
DISPLAY(mul3(3)(4)(5)); // => 60
DISPLAY(mul3(4)(5)(6)); // => 120
DISPLAY(mul3(5)(6)(7)); // => 210
return 0;
}

由于使用了模板特化来实现柯里化,为了保证编译不出错(变长模板展开需要有一个终止条件),所以在特化时我们只处理了没有参数和只有一个参数的情形

这样也严格的执行了柯里化的定义,即函数永远只接收一个参数,同时也确保了返回值是一个函数对象而不是之前那种包装了参数数据的特殊数据结构

函数组合

引入管道概念

管道是linux中一种特殊的文件,程序可以读取管道的数据,也可以把数据写到管道里传递给其他程序

比如ps -aux --sort -pcpu | less是最经典的检查CPU占用的命令

我们可以粗略的认为,运算符|接收两个参数lhsrhs,左端提供数据,右端提供函数

重载|运算符

1
2
3
4
template <class Arg, class F>
auto operator|(Arg && arg, F && f) -> decltype(f(std::forward<Arg>(arg))) {
return f(std::forward<Arg>(arg));
}

可以写出这样的代码

1
2
3
4
5
int result = 5 | [](int _) { return _ * 2; };

cout << (2 | [](int _) { return _ + 5; }
| [](int _) { return _ * 2; }
| [](int _) { return "result= " + std::to_string(_); }) << endl;

如果没有lambda支持,同样的功能可能单是函数声明就十分麻烦,千言万语不及一句随用随写

只要管道不要数据

现在假设我们有这样的一个管道链a | f | g,我们不再关心数据a,而关心函数的组合| f | g

其实就是f(g(x))

注意到每一个函数的类型都可能是不同的,而这是一个线性结构,所以选用tuple作为核心容器记录所有的函数;同时,对外公开两个方法composite()operator()用于处理新函数和执行函数调用,内部则用递归的方法把函数一次调用

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include<iostream>
#include<string>
#include<tuple>
#include<functional>

using namespace std;

template <class Arg, class F>
auto operator|(Arg && arg, F && f) -> decltype(f(std::forward<Arg>(arg))){
return f(std::forward<Arg>(arg));
}

template <class... Fs>
class composited_functor{
private:
const std::tuple<Fs...> fs_;
const static std::size_t _size_ = sizeof...(Fs);

template <class Arg, std::size_t I>
auto call_impl(Arg && arg, const std::index_sequence<I>&) const
-> decltype(std::get<I>(fs_)(std::forward<Arg>(arg))){
return std::get<I>(fs_)(std::forward<Arg>(arg));
}

template <class Arg, std::size_t I, std::size_t... Is>
auto call_impl(Arg && arg, const std::index_sequence<I, Is...>&) const
-> decltype(call_impl(std::get<I>(fs_)(std::forward<Arg>(arg)), std::index_sequence<Is...>{}))
{
return call_impl(std::get<I>(fs_)(std::forward<Arg>(arg)), std::index_sequence<Is...>{});
}

template <class Arg> auto call(Arg && arg) const
-> decltype(call_impl(std::forward<Arg>(arg), std::make_index_sequence<_size_>{})){
return call_impl(std::forward<Arg>(arg), std::make_index_sequence<_size_>{});
}
public:
composited_functor() : fs_(std::tuple<>()) {}
composited_functor(std::tuple<Fs...> && fs) : fs_(std::forward<decltype(fs)>(fs)) {}

template <class F>
inline auto composite(F && f) const{
return composited_functor<Fs..., F>(std::tuple_cat(fs_, std::make_tuple(std::forward<F>(f))));
}

template <class Arg>
inline auto operator()(Arg && arg) const{
return call(std::forward<Arg>(arg));
}
};

template <class... Fs, class F>
auto operator,(composited_functor<Fs...> && composited, F && f){
return composited.composite(std::forward<F>(f));
}


int main(int argc, char**argv) {
cout << (
2 | ( composited_functor<>()
, [](int _) { return _ + 5; }
, [](int _) { return _ * 2; }
, [](int _) { return "result = " + std::to_string(_); } )
)
<< endl;
// result = 14
return 0;
}

haskell里用.运算符来表示函数组合,因为C++不允许重载这个操作符…….

这不是bug,这是feature

于是这里重载了逗号运算符

这种写法…composited_functor<>()太碍眼了,完全可以不需要的,我们换个思路重新考虑一下这个问题

特殊的应用符

Haskell里有一个特殊的运算符$来消除括号,同样地我们也可以引入C++中,Haskell中$定义如下

1
2
($) :: (a -> b) -> a -> b
f $ x = f x

换句话说就是可以不写括号,直接把数据应用到函数上

由于C++不能创建新的运算符,只好曲线救国,换一个运算符来实现功能

这里选择operator<<

注意需要function_traits.hpp头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include<iostream>
#include<string>
#include<tuple>
#include<functional>

#include"./function_traits.hpp"

template <class Arg, class F>
auto operator|(Arg && arg, F && f) -> decltype(f(std::forward<Arg>(arg))){
return f(std::forward<Arg>(arg));
}

// ($) :: (a -> b) -> a -> b
// f $ x = f x
template <class Return, class Arg>
auto operator<<(std::function<Return(Arg)> && f, Arg && arg){
return f(std::forward<Arg>(arg));
}

template <class Return>
auto curry_impl(std::function<Return()> && f){
return std::forward<decltype(f)>(f);
}

template <class Return, class Arg>
auto curry_impl(std::function<Return(Arg)> && f){
return std::forward<decltype(f)>(f);
}

template <class Return, class Arg, class... Args>
auto curry_impl(std::function<Return(Arg, Args...)> && f){
auto _f = [f = std::forward<decltype(f)>(f)](Arg arg){
std::function<Return(Args...)> rest
= [f = std::forward<decltype(f)>(f), arg = std::forward<Arg>(arg)]
(Args... args)->Return { return f(arg, args...); };
return curry_impl(std::forward<decltype(rest)>(rest));
};
return __utils::make_func(_f);
}

template <class Func> auto curry(Func && f){
auto _f = __utils::make_func(f);
return curry_impl(std::forward<decltype(_f)>(_f));
}


// (.) :: (b -> c) -> (a -> b) -> a -> c
// f . g = \x -> f $ g x
template <class MaybeA, class MaybeB, class MaybeC>
auto operator<<(std::function<MaybeC(MaybeB)> && f, std::function<MaybeB(MaybeA)> && g)
-> std::function<MaybeC(MaybeA)>{
std::function<MaybeC(MaybeA)> composited
= [f = std::forward<decltype(f)>(f), g = std::forward<decltype(g)>(g)]
(MaybeA arg)->MaybeC { return f(g(arg)); };
return __utils::make_func(composited);
}


int main(int argc, char**argv) {
std::cout << ( curry( [](int _) { return _ + 5; } ) << 4 )
<< std::endl;
// 9

std::cout <<
(
curry( [](int _) { return "result= " + std::to_string(_); } )
<< curry( [](int _) { return _ * 2; } )
<< curry( [](int _) { return _ + 5; } )
<< 4
)
<< std::endl;
// => result= 18

return 0;
}

很像 Haskell 的 f $ g $ h x ,不过得益于 lambda 代码不至于过于冗长

现在我们手上已经有了柯里化和函数组合这两个工具了。借助这两个工具,我们可以非常自然地把逻辑用函数来进行抽象,把参数的传递用柯里化来代替,从而更简单的组合我们的代码,更大限度的减少不良代码。这里需要再次强调一次,那就是DRY 原则,不要反复重复没用的代码。编程的最大乐趣在于一层一层的把逻辑抽象,从而用简洁的代码,通过一层一层的组合、堆叠,完成一个又一个复杂的任务。可能之前我们手上只有一种实现的方法,而现在道路多了之后,就会大大地解放我们的思路,方便我们思考出最适合自己的一条实现方案。

吾头在否?