Jump to Navigation

PHP中的函数式编程特性分析

一、引言
在写此文时,想起之前看过的一句话,如果要学习一门新的语言,那么就学习一门能够改变你的思维方式的语言。
本着这句对我触动比较大的一句话,一直在关注着LISP/Scheme这类直接产生函数式编程方式的语言。
在这中间看过一些相关的资料,试着编写过一些代码,却一直停留在学习试验阶段,很难写出像样的可用程序来。

在最近几年中,又是一个计算机界推出新语言高潮。比较新的有Closure,Go等。
并且一些比较老的语言像C++,Python,Perl,发展变化的步伐也变大了。
在C++11中,也添加了匿名函数的支持。nodejs的javascript语言,更是标榜着“披着C语言外衣的LISP”。
从这些变化除了让程序更高效,功能更丰富全面外,也提供了大量使用函数式思维解决程序复杂度不断上升的问题的特性。

这也正好契合了我希望能学习一门新语言的想法。不过这门新语言并没有找到,而是在这个过程中,
通过分析多种编程语言的特性和多种发展迅速的语言的新特性,有了一些新发现:
函数式思维才是我要找的东西,函数式思维不只是标榜为函数式语言才有的东西。
把函数式思维用于现有的命令式语言(一般也都是混合模式的语言),一样能够加深对函数式思维的理解。

其实经过了这个复杂的寻找过程,也影响了我对LISP/Scheme类函数式语言的理解。
现在也能用Scheme编程简单实用程序了。
不过现在,还是从我比较熟悉的其中一门语言PHP说起,探讨函数式编程的基本应用。

二、PHP目前的发展现状
说到函数式编程,不得不提到闭包的概念,而PHP是从PHP-5.3版本才引入的,
所以PHP语言在函数式编程方面特性并不强,不过现在已经能够初步实现一些函数式编程方式了。
并且,就目前对此刚入门来说,如果要分析一门包含所有函数式编程特性的语言,
分析起来也还有不少困难。

除了现在稳定版本的PHP-5.x版本,在函数式语言方面算是非常初步的。
从phpng的开发看,也就是后续的6.0、7.0版本,并没有看到太多的函数式编程特性的影子。
反而是Facebook的基于PHP的Hack/HHVM语言,已经添加了许多函数式编程的特性语法。

三、使用闭包匿名函数实现数组求和
先来看一下在PHP中使用闭包的方式,

  1. $fn = function($a, $b) { return $a + $b; };
  2. echo $fn(1, 2);
  3.  

这里实现了把一个类似函数的语句赋值给一个变量,然后这个变量执行'()'操作。
最终的输出结果为3。
乍一看,这也没有什么了不起的,和函数的功能一样的。
接下来,我们用这种方法实现一个求和功能,看下这种方式的优势。

  1. $arr = array(1, 2, 3, 4, 5);
  2. $sum = array_reduce($arr, $fn, 0);
  3. echo $sum;
  4.  

这段代码的输出结果为15。
其执行过程为给定一个初始值,遍历数组,并调用这个$fn变量所表示的匿名函数,然后返回结果。
在前一段代码中,$fn()直接调用输出结果。
在这个例子中,$fn作为一个参数传递给了另一个函数,由这个函数隐匿调用$fn($a, $b)。
这比直接使用for循环遍历数组要简洁的多。
其中的核心思想是把通用的算法作用于数据,产生相应的结果,着重点在通用算法上。

当然,在PHP中,对于这个数组,还可以使用array_sum函数直接计算出结果。
但是,由于可能的计算操作非常多,或者数组不是这种标准格式的,现有的函数就不够用了。
单独靠PHP需要提供函数也可就多了,而且全部都提供的话PHP也就没这么容易入手了。
这些离散的PHP函数,对核心开发人员来说还要考虑是否真的需要。

四、具有更丰富函数式编程特征的求和方式
上一节中,我们使用了针对array类型的array_reduce函数实现了求和功能。
本节中,我们将编写一个更通用的遍历数据的函数map实现任意数据类型的遍历语义。
同时,提供一个更通用的reduce函数,将相同的算法用于更复杂的不同的数据结构中。

通用的遍历函数map的实现,

  1. function map($var, $proc)
  2. {
  3. if (!is_callable($proc)) return false;
  4.  
  5. if (is_array($var)) {
  6. foreach ($var as $k => $v) {
  7. call_user_func($proc, $k, $v);
  8. }
  9. } else if (is_string($var)) {
  10. for ($i = 0; $i < strlen($var); $i ++) {
  11. call_user_func($proc, $var[$i]);
  12. }
  13. } else {
  14. }
  15.  
  16. }
  17.  

目前该map函数不但能够遍历数组,还能够遍历字符串,然后把遍历出来的每个元素逐个应用于可调用的$proc上。
这就是函数式编程中的关注要做什么,map函数就是提供遍历,而遍历什么数据结构则放在其次。

实现聚合的通用的reduce函数实现,

  1. function reduce($var, $proc, $default)
  2. {
  3. $val = $default;
  4. map($var, function ($k, $v) use (&$val, $proc) {
  5. $val = call_user_func($proc, $val, $v, $k);
  6. return $val;
  7. });
  8.  
  9. return $val;
  10. }
  11.  

聚合首先要遍历,所以使用了map函数,在些函数基础出做进一步函数调用。
reduce函数同样也是关注了要做什么。

现在来看怎么用函数式编程方式实现求和,

  1. function add()
  2. {
  3. $args = func_get_args();
  4.  
  5. return Funt::reduce($args, function ($r, $k, $v) {
  6. return $r + $v;
  7. }, 0);
  8. }
  9.  

  1. $sum = add(1, 2, 3, 4, 5);
  2.  

或者

  1. $sum = add($arr);
  2.  

是不是非常简单呢,甚至可以通过完善add函数实现不同数据类型的求和。

这段代码的最大特点是什么呢?是要做什么为主,这个说清楚了,后面才是怎么做。

五、 PHP中函数式编程特性的不足与补充

在以上两节,从PHP引入的闭包/匿名函数开始,介绍基本使用,并通过示例演示函数编程的基本方式。
不过,如果再要使用更多的函数编程特性的话,PHP目前就显得力不从心了。
像在Hack语言中提供的“表达式闭包”,它是简化版本的匿名函数,由解释器自动当作函数执行。
如在Hack语言中遍历数组并打印, map($arr, ($k, $v) ==> print $k . " --> " $v);
这在PHP中很难以做到。

还有通用算法方面,虽然有些涉及,但也不够完善和通用。
不过在下一节中,我将试着用一种比较难看的方式实现类似的功能,
变通地为PHP添加更多的函数式编程特征,同时也给出一种在此基础上再次扩展的思路。

六、扩展PHP的函数式编程特性

首先介绍强大的O函数,它让所有PHP变量成为一个StdClass对象,然后捕捉在这个对象上的所有调用,
从而实现任意变量的函数调用。

  1. function O($var)
  2. {
  3.  
  4. // 引入这个类,保证使用方无论保证这个引用,还是一直创建新的对象,都能够正确调用到相应方法
  5. // 这个类的代价相对更小
  6. if (!class_exists('__StdClass')) {
  7. class __StdClass extends StdClass
  8. {
  9. private $obj = null;
  10. public function __construct($var)
  11. {
  12. $this->obj = $var;
  13. }
  14.  
  15. public function __call($m, $a)
  16. {
  17. $ho = Oimp::__get_o_handle($this->obj);
  18. return call_user_func_array(array($ho, $m), $a);
  19. }
  20. };
  21. }
  22.  
  23.  
  24. $obj = new __StdClass($var);
  25.  
  26. return $obj;
  27. };
  28.  

这个函数让我们能实现类似ruby中5.times()这种功能。
O(5)->times(function ($i) { print("say $i"); });
是不是很强大。
这个函数中使用到的Oimp类实现请查询完整的源代码文件。
除了在Oimp中实现times方法,只要不断扩充Oimp实现新的方法,就能够提供更多具有函数式特征的功能。

另外一点,现在来看一下“表达式闭包”的模拟,

  1. // lambda('$x, $y => aaaaa'
  2. // @return Closure object
  3. function lambda($body)
  4. {
  5. $margs_str = trim(explode('=>', $body)[0]);
  6. $margs_list = explode(',', $margs_str);
  7. $mbody = trim(explode('=>', $body)[1]);
  8.  
  9. $f = function(...$_lambda_args) use ($margs_list, $mbody) {
  10. $code = "<?php\n\n";
  11.  
  12. foreach ($margs_list as $_lambda_k => $_lambda_v) {
  13. if (empty($_lambda_v)) continue;
  14. $code .= "" . trim($_lambda_v) . " = \$_lambda_args[{$_lambda_k}];\n";
  15. }
  16.  
  17. $code .= "\nreturn ( " . $mbody . " );\n";
  18.  
  19. $code_hash = md5($mbody);
  20. $fname = "/tmp/php_lambda_{$code_hash}.php";
  21. file_put_contents($fname, $code);
  22.  
  23. $lv = require($fname);
  24. unlink($fname);
  25.  
  26. return $lv;
  27. };
  28.  
  29. return $f;
  30. }
  31.  

它使用PHP的require解释写到临时文件的表达式代码,实现类似eval的功能。
但应该比eval要轻量级一些,并且功能上也更容易控制。

有了这个函数,本节开始时的示例可以实现如下,

  1. map($arr, lambda('$k, $v ===> print($k . " ---> " . $v)'));
  2.  

由于无法在这一层级上修改PHP的语法,只能使用传递字符串做再次解析的方式。

七、总结

现在的PHP已经支持一点函数式编程特性,不过却没能很好的组织起来。
这体现了在函数式编程方面PHP语言还有很长的路要走。
通过一些额外的方法能完善PHP的函数式编程特性,
即使有本文实现的基本框架,还有很多工作要做。

参考:
http://hhvm.org/
http://php.net/

添加新评论

Plain text

  • 不允许HTML标记。
  • 自动将网址与电子邮件地址转变为链接。
  • 自动断行和分段。
CAPTCHA
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.
Image CAPTCHA
Enter the characters shown in the image.


Main menu 2

Story | by Dr. Radut