PHP编程之函数式编程与数组

在函数式编程的世界里,针对集合的操作有三大类,分别是:映射、过滤和归约。
虽然PHP是一门解释性脚本语言,并且支持面向过程编程和面向对象编程,与函数式编程还是有很大区别的,但是PHP也为映射、过滤和归约提供了对应的函数。它们分别是:

  • 映射:array_map()
  • 过滤:array_filter()
  • 归纳:array_reduce()

下面可以通过一个简单的例子来快速认识这三大类的操作。假设我们有1、2、3、4、……、9、10 这样十个数字,作为最初的集合元素。则有:

<?php
$data = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

如果我们想让每个数字都翻N倍,即对原来每个元素,都通过一个规则映射到一个新的集合,那么这就是映射。使用array_map()函数可轻松实现这一点。

// 集合的映射操作
// 假设翻2倍
$n = 2;  
$dataDouble = array_map(function ($value) use ($n) {
    return $value * $n;
}, $data);

print_r($dataDouble);

通过映射后,新集合与旧集合的元素是一一对应的,数量不会少也不会多。这就和投影一样。这里的执行结果会输出:

Array
(
    [0] => 2
    [1] => 4
    [2] => 6
    [3] => 8
    [4] => 10
    [5] => 12
    [6] => 14
    [7] => 16
    [8] => 18
    [9] => 20
)

而过滤操作,显然会对集合的元素数量有影响,通常都会根据条件过滤、移除部分元素,所以执行过滤后,元素个数会减少。继续前面的示例,如果要找出全部的奇数,可以:

// 集合的过滤操作
$dataOdd = array_filter($data, function ($value) {
    return ($value % 2) == 1;
});

print_r($dataOdd);

输出结果是:

Array
(
    [0] => 1
    [2] => 3
    [4] => 5
    [6] => 7
    [8] => 9
)

最后,是集合的归约操作,即把全部的元素通过某个规则全部合成一个东西。注意,映射和过滤后的新元素的类型,和原来元素的类型是一样的。即原来是整型的元素,操作后还会是整型的元素,虽然数量上会有所变化。而归纳则是完全不同的操作,除了最后元素个数变成只有一个外,新元素的类型极大可能会新的类型。
拿这里的例子来说,假设我们要把全部的数字按字符串连接起来,那么最后就会形成一个字符串:12345678910。使用array_reduce()的实现代码是:

// 集合的归约操作
$str = array_reduce($data, function ($carry, $item) {
    return $carry . $item;
}, '');

echo $str, PHP_EOL;

在函数式编程的世界里,函数是“一等公民”。而在OOP世界里,类才是“一等公民”。在最开始时,我们需要对原来的数值翻N倍,这个N倍暂时是通过参数来传递的。又如,在过滤奇数时,是直接硬编程在代码里的,如果我要改成过滤偶数呢,怎么办?又假设我会动态传入过滤条件呢,又怎么办?大家可以回想一下,在面向过程编程和面向对象编程里,我们都是怎样解决类似这样的需求的。
这里简单分享一下,在函数式编程世界里巧妙的做法。因为函数是它们的“一等公民”,函数可以作为函数的参数也可以作为函数的返回值,甚至还可以作为变量存储起来,这叫做高阶函数。通过原子的操作,再结合部分施用或柯里化,就能灵活组合成复杂的功能。
初次接触函数式编程的同学,对于柯里化这个概念有点陌生。这里稍微解释一下,简单地理解,可以看成一开始某个函数需要两个参数的,但我先提供一个参数,然后返回一个函数先。剩下的另外一个参数,再作为参数传给这个新的函数。打个比方,就类似分期付款一样,一开始钱不够,先给部分,后面再分开给。以翻N倍为例,这里有两个参数,一个是原始的数值,另一个参数是翻多少倍。但再一推导,实际上就是两个数相乘。
先来一睹,使用Scala函数式编程语言实现上面同样功能的完整代码。将下面Scala代码保存到ArrayCurry.scala文件,位置任意。

/**
 * 使用Scala的同等实现
 */

object ArrayCurry {
    def main(args: Array[String]): Unit = {
        val data: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

        // 集合的映射操作
        val dataDouble = data.map(x => x * 2);
        println(dataDouble.mkString(" "))

        // 集合的过滤操作
        val dataOdd = data.filter(_ % 2 == 1)
        println(dataOdd.mkString(" "))

        // 集合的归约操作
        val str: String = data.foldLeft("")((x, y) => x.toString() + y.toString())
        println(str)
    }

在安装了Scala的情况下,可以执行以下命令:

$ scalac ./ArrayCurry.scala  && scala ArrayCurry

然后就可能看到输出:

2 4 6 8 10 12 14 16 18 20
1 3 5 7 9
12345678910

效果和前面是一样的,除了输出格式外。但Scala的代码会更简洁、更优雅。
再来稍候看一下柯里化的应用。先添加一个柯里化实现的类成员函数:

object ArrayCurry {
    // 柯里化
    def curry[A, B, C](f: (A, B) => C) : A => (B => C) = {
        (a: A) => ((b: B) => f(a, b))
    }
}

然后在main()函数最后追加以下代码,就可以应用柯里化实现对不同倍数的相乘。

object ArrayCurry {
    def main(args: Array[String]): Unit = {
        // ……
        // 两数相乘
        def multi(left: Int, right: Int): Int = {
            left * right
        }

        val multi2 = curry(multi)(2)
        println(data.map(multi2).mkString(" "))
    }

从这里可以窥探到函数式编程的思想。即找出原子性的操作,再基于此基础推导出更多衍生的操作。例如这里的,两数相乘multi是最基本的操作,其次再到翻2倍的multi2,当然基于multi我们可以快速创建翻3倍、翻10倍、翻N倍的函数——只需要传一个参数即可。最后,把这个翻2倍的因子传递给映射操作,就可以完成对整个集合的映射操作。多么微妙!
至此,我们简单回顾一下。一开始,我们学习了PHP数组关于映射、过滤和归约的三大类操作。然后使用函数式编程语言Scala同样实现了一遍,还稍微领略了函数式编程的魅力和思想。但这本书更多是关于PHP的书,所以这里要告诫各位开发同学的是,虽然函数式编程的魅力很大,但PHP语言更多是简单、实用、方便。如果没有必要,建议不要采用array_map()、array_filter()、array_reduce()这些函数。
有同学开始感到困惑了,讲了那么多,最后是为了让我们不要使用?为什么呢?因为匿名函数会增加代码的复杂度,增加代码的嵌套层级,不容易理解,更不容易维护。现在的时代不再是以前写出别人看不懂的代码就是高手,而是如何写出连实习生都能快速理解的代码才是专家。毕竟PHP没有Scala那么强悍的语法糖,也没有像Javascript的上下文,过度使用这类数组的函数,反而会适得其反。
最后,我们来看下,PHP回归到简单版的实现方式。其实很简单,改用foreach进行循环处理即可。不需要花俏、华而不实的外表。以下代码,想必连刚学PHP的同学也能快速理解,并且维护起来成本更低。

// 翻2倍
$dataDouble = array();
$n = 2;
foreach ($data as $it) {
    $dataDouble[] = $it * $n;
}   
print_r($dataDouble);

// 取奇数
$dataOdd = array();
foreach ($data as $it) {
    if ($it % 2 == 1) {
        $dataOdd[] = $it;
    }
}   
print_r($dataOdd);

// 字符串拼接
$str = '';
foreach ($data as $it) {
    $str .= $it;
}   
echo $str, PHP_EOL;

如果非得要用一句话来总结,那就是:不要让事情变得更复杂,不要增加人为的偶然复杂性。

发表评论