思考并回答以下问题:
- 函数式编程是什么样子的代码?
- 绝大多数的for、foreach和while都可以用数组函数代替吗?
- foreach有代码副作用怎么理解?array_walk有吗?
- 数组内的值互相比较,累加的时候用array_reduce。怎么理解?
旧代码
1 | for ($index = 0; $index < count($scope) -1; $index++) |
这段代码实现了两个功能。
第一个是在一个从数据库中读取的列表数组中找出某个值是最大的一条记录, 并且把这个最大的值和跟这个值相关的时间给取出来。
第二个比较复杂,是将这个列表数组中的值映射到另外一个列表数组中,可以把这个过程看作是SQL中的JOIN操作,只是JOIN的条件异常复杂 ,在这里我也不详述了,阅读的同学也不必去深入探究。
重构
1 | $maxInfo = array_reduce($rs, function($result, $item){ |
从代码结构上来看, 重构后的代码的确清晰了不少。
我将原本拥挤在一起的两个功能进行了拆分,上面部份是求最大值,下面部份是对两个数组进行映射。这里我用到了两个PHP中数组的函数array_map和array_reduce,这篇文章想表达的思想就是利用此类函数来提高PHP代码的可读性。这类函数主要包括以下4个函数:
- array_filter
- array_map
- array_walk
- array_reduce
这4个函数威力巨大, 在处理列表数组方面可以完全替换掉for、foreach、while这些循环控制语句,这也是函数式编程方式在PHP的一部份体现。
数组函数
array_filter
1 | $data = [ |
这段代码将数组中性别字段为女的数据项提取出来。这是原汁原味的命令式程序代码。
如果data变量中的数据并非存放于php数组中,而是存在于关系数库的表之中,那如何取得性别为女的数据结果呢? 对于程序员来说这貌似是一个更加简单的问题,一句SQL语句就搞定了。1
select * from data where 性别='女';
显然, 利用SQL查询数据更加方便,意途也更加清晰,毕竟一个SQL表达式就将所有的程序逻辑都给表达了现来。这句SQL只表达了:“我需要性别为女的数据,至于怎么拿,我不管”,除了结果,其它的它一概不知。
我们不妨把这种思路引入到PHP程序设计之中,不也意味着我们的PHP程序的逻辑表达也更加清晰,代码的可读性也更高。所幸, 这种利用表达式编程的方法在PHP中也完全可以实现。1
2
3$result = array_filter($data, function($item){
return $item['性别'] == '女';
});
利用array_filter函数,可以轻松的完成这个任务, 仔细观察一下, 是不是原来的程序逻辑都不见了,包括定义数组、循环、条件判断这些都不见了,逻辑方面是只剩下了一个性别比较语句,这对于代码所实现的功能一目了然。 和上面的SQL比较一下,这里的性别判断语句就是SQL中where子句后面的条件判断,而array_filter函数其实就是SQL中的where子句。 这就是SQL语句面向结果编程的逻辑原封不变的在PHP中的体现,也就是时下最流行的“声明性编程”或者也称为“表达式编程”。
此外,代码中性别判断语句所在的位置称之为lambda表达式, 更通俗一些的叫法是匿名函数。不难看出,在SQL的where条件中编写条件判断远不如在匿名函数中写PHP代码来的灵活,在where条件中只能执行or和and逻辑,而在php匿名函数中可以随便怎么写,只要函数的返回值是个布尔值就可以了,这也是php声明性编程优于SQL声明性编程的地方。
array_map
1 | $result = []; |
数据中的性别字段是中文的,值也是中文的,现在想把字段名和字段值都改为英文的,就可以用上面这段代码实现。
下面是利用SQL的实现方式:
1 | select id, name case 性别 when '男' then 'male' else 'female' end as gender from data; |
SQL中case when语句好像不太好看, 但是不影响整体逻辑的表达。将这段SQL转换成PHP的方式实现。1
2
3
4
5
6
7$result = array_map(function($item){
return [
'id' => $item['id'],
'name' => $item['name'],
'gender' => $item['性别'] == '男' ? 'male' : 'female'
]
}, $data);
相比之前的PHP实现, 是不是简洁明了了许多。
在这里使用到了array_map函数。在SQL语句中以select语句最为常用,select的字面意思是“选择”,而select语句也被称之为选择查询,事实上从关系数据库的角度来说,select被称之为“投影”,并不是查询什么的。换言之,select语句只是将SQL的查询结果以一定的方式(选字段、计算值等等)提取出来了。php中的array_map表达的也是这层意思,“映射”与“投影”完全是一种意思的不同表达。
很多情况下,迭代的数据一般为一个二维关联数组,但array_map本身在回调函数的函数体内无法获取到关联数组的下标的。
目的:在回调体内获取迭代关联数组的下标值。
解决:可以把关联数组的下标通过array_map()的第三个形参把下标传进去。
1 |
|
array_walk
array_walk函数没有像array_map和array_filter那样深刻的意义,但是它在设计可读性良好的代码时也是不可或缺的。
array_walk是for或foreach语句的替代函数。1
2
3
4
5
6
7
8foreach ($data as $item)
{
echo $item['id'];
}
array_walk($data, funcname($item){
echo $item['id'];
})
以上代码分别是foreach和array_walk对于遍历数组的实现方式。看起来,好像array_walk的实现方式更加复杂,但是在更深层次的语义方面foreach表达的是循环遍历,但是在这个循环的过程中,要做什么样的处理,是没有任何约束的, 删除被遍历的数组的某一项,或者修改一个十万八千里以外的变量的值,这便是所谓的“代码副作用”,俗话说“白蚁虽小,危害无穷”,当这些看似微不足道的副作用发展壮大时,便会给程序员维护程序代码带来的障碍是致命的。
而array_walk函数缺省情况下所有执行代码的作用域都在匿名函数内,如果要依赖或操作函数之外的数据,必须通过匿名函数的use关键字导入。通俗一点的请,array_walk函数的权限不如foreach来的大,因此,使用array_walk函数后,虽然无法让你随心所欲的编程,但是大限度的减少了你代码的副作用,两相权衡array_walk所带来的好处还是有值得使用它的理由的。
首先,大多数时候写代码根本不需要太大的“权限”,其次,把代码所影响的范围控制到最小好处不言而喻。微信张小龙讲过,微信做的最好的一点便是“克制”,我们写代码又何尝不是。这一点array_filter和array_map中也有体现,宽泛的讲,所有使用匿名函数的地方都能享受到这个好处。
array_walk所表达的语义就是“假如你需要用到我,那么你除了遍历以外,其它的事情最好都别干,否则你还是去用原生的foreach吧”。
第3个参数
1 | array_walk(array, myfunction, parameter...) |
第3个参数规定用户自定义函数的参数,可以为函数设置一个或多个参数。
1 | $color = [ |
输出结果:1
2
3a has the value red
b has the value green
c has the value blue
向二维数组中追加元素
1 | $addDataArr = [ |
输出结果:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20Array
(
[0] => Array
(
[order_id] => 6
[add_time] => 2020-03-05 19:50:05
[add_time_sync] => 2020-03-05 19:54:17
[status] => 0
[item_id] => 24353454
)
[1] => Array
(
[order_id] => 7
[add_time] => 2020-03-05 19:52:12
[add_time_sync] => 2020-03-05 19:54:17
[status] => 0
[item_id] => 12454758
)
)
array_reduce
array_reduce是上面所讲的三个函数的集大成者,这三个函数的底层完全可以由array_reduce实现。
先看一下下面的php代码。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 求最大年龄
$maxAge = 0;
$result = null;
foreach ($data as $item)
{
if($item['age'] > $maxAge)
{
$maxAge = $item['age'];
$result = $item;
}
}
// 计算平均年龄
$total = 0;
foreach ($data as $item)
{
$total += $item['age'];
}
$avg = $total / count($data);
常规的PHP写法,代码分别用于计算数组记录中平均年龄和最大年龄,代码需要循环数组,并把计算结果存入一个标量(单个值,区分于列表变量)。
假如要以表达式编程的方式完成编写这两个功能,利用array_filter、array_walk、array_map三个函数是很难一步到位实现的。
于是,就到了array_reduce大显身手的时候了。1
2
3
4
5
6
7$result = array_reduce($data, function($curr, $item){
return $curr['age'] < $item['age'] ? $item : $curr;
}, ['age' => 0]);
$avg = array_reduce($data, function($curr, $item){
return $curr + $item['age'];
}, 0) / count($data);
上面的代码是求平均年龄和最大年龄的表达式编程的实现,如果对array_reduce函数的工作机制不了解,看上面两段代码会觉得在看天书。1
2
3
4
5
6
7
8function array_reduce($data, $callback, $initial)
{
foreach ($data as $index => $val)
{
$initial = $callback($initial, $val);
}
return $initial;
}
这是array_reduce函数的实现代码,函数有3个参数,3个参数的作用分别是:
- 第一个参数$data,就要是处理的数据源。
- 第二个参数$callback,循环遍历时会被调用的函数,函数返回的结果在下一次循环调用时会被再次当成参数传入。
- 第三个参数$initial,作为$callback函数被初次调用时的参数传递。
再来一个递归版本的array_reduce实现,帮助更好的理解这个函数的使用意义。1
2
3
4
5
6
7
8
9
10function array_reduce($data, $callback, $initial)
{
if(count($data) == 0)
{
return $initial;
}
$item = array_shift($data);
$initial = $callback($initial, $item);
return array_reduce($data, $callback, $initial);
}
善用array_reduce函数几乎可以替换掉绝大多数需要使用foreach、for、while语句的代码。
1 | $arr = [ |
在标准的函数式编程语言中,是没有循环控制语句的,假如要进行循环计算,都是使用此类函数来实现的,如果某些极端的情况下这些函数无法满足需求,那么就以手动写递归来实现循环,以达到表达式编程的目的。
array_walk与array_map区别
1、array_map里面的函数可以是自定义函数,也可以是php自带的函数,比如trim去除空格等。而array_walk里面的函数只能是自定义的函数。
2、array_map必须要有返回值,因为要填充数组。而array_walk可以没有返回值,输出的话要在调用的函数中通过echo来输出。
3、map主要是为了得到你的回调函数处理后的新数组,要的是结果。walk主要是对每个参数都使用一次你的回调函数,要的是处理的过程。
总结
1.通过函数本身的意义就能表达出代码实现了什么样的功能,而不用去琢磨代码具体细节来理解代码的作用。
2.表达式编程相对于命令式编程能极大的简化功能的实现过程, 提升编码效率。
3.表达式编程对于代码的可读性、可维护性具有非凡的意义。
4.利用匿名函数控制代码的副作用。
5.由传统的面向过程式程序设计向现代化的函数式编程靠拢。
通过前面示例的讲解,利用这4个函数实现的代码相对于传统的实现方式并没有不可思议的变化,然而,当需要解决的问题复杂到一定程度时,合理利用这4个函数会使代码的复杂性大规模下降。