优化函数式编程:向 PHP 移植 Clojure 函数
许多通用程序设计语言试图兼容大多数编程范式,PHP 就属于其中之一。不论你想要成熟的面向对象的程序设计,还是程序式或函数式编程,PHP 都可以做到。但我们不禁要问,PHP 擅长函数式编程吗?本文系国内 ITOM 管理平台 OneAPM工程师编译整理。
笔者在今年冬天开始时,在 Recurse Center致力于学习 Clojure,更加深入地了解了函数式编程,并重新拾起 PHP 的客户端工作。但笔者仍然希望运用一些高阶函数和概念,并对它们进行研究。
笔者已经在 PHP 中实施了模拟 LISP 语言,并看到了一些在 PHP 中通过使用 underscore 类库以兼容某些关键函数方法的尝试。但为了使 Clojure 在写入其它编程语言时仍然保有较高的速度,笔者特意镜像 Clojure 的标准库,以使自己能在编写真正的 PHP 代码时,以 Clojure 的方式思考。虽然在学习的过程中绕了一些弯路,笔者仍然愿意向各位展示自己是如何实现 interleave 函数的。
幸运地是,已经有人执行了 array_some 和 array_every,并且非常地道(至少笔者这么认为)。
/**
* Returns true if the given predicate is true for all elements.
* credit: array_every and array_some.php
* https://gist.github.com/kid-icarus/8661319
*/
function every(callable $callback, array $arr) {
foreach ($arr as $element) {
if (!$callback($element)) {
return FALSE;
}
}
return TRUE;
}
/**
* Returns true if the given predicate is true for at least one element.
* credit: array_every and array_some.php
* https://gist.github.com/kid-icarus/8661319
*/
function some(callable $callback, array $arr) {
foreach ($arr as $element) {
if ($callback($element)) {
return TRUE;
}
}
return FALSE;
}
我们只要简单地取消调用 every 函数,就可以运用 not-every 函数插入一些容易实现的目标,同时仍然有相同 signature。
/**
* Returns true if the given predicate is not true for all elements.
*/
function not_every(callable $callback, array $arr) {
return !every($callable, $arr);
}
如你所见,笔者已经去掉了前缀 array_。PHP 的不便之处在于强调序函数,通常使用前缀 array_ 来运行数列。笔者将此理解为这两种函数的作者是在相互模仿。虽然数列在 PHP 中已经形成事实数据结构,但标准数据库以此种方式被写入并不常见。
这一标准适用于基本高阶函数,你可以使用 array_map、array_reduce和 array_filter 结尾,而不是 map,recude 和 filter。如果这些还不够,那参数便不一致了。array_reduce 和 array_filter 都以数列为第一个参数,然后以回调值作为第二个参数,首先调回 array_map。在 Clojure 中,通常首先运行回调函数,所以让我们将这些函数重新命名,然后只需一步就能使这些签名变得正常:
/**
* Applies callable to each item in array, return new array.
*/
function map(callable $callback, array $arr) {
return array_map($callback, $arr);
}
/**
* Return a new array with elements for which predicate returns true.
*/
function filter(callable $callback, array $arr, $flag=0) {
return array_filter($arr, $callback, $flag);
}
/**
* Iteratively reduce the array to a single value using a callback function
*/
function reduce(callable $callback, array $arr, $initial=NULL) {
return array_reduce($arr, $callback, $initial);
}
我们目前没有其它方法,所以当 Clojure 中的 reduce 函数通过了初始值并作为第二个参数时,它便有了另一个签名。鉴于此,我们从现在开始就将 initial 作为最终值——毕竟相对于原函数来说,这仍然是一大进步。另外,我们也将在过滤函数中保留 $flag,它决定了是否全部通过键和值,还是只通过键。
在 Clojure 中,first 和 last 是十分有用的两个函数,相当于 PHP 中的 array_shift 和 array_pop。它们的关键不同之处在于:PHP 中两个命令具有毁坏性。以 array_shift 为例,它返回数列的第一项,同时又从原始数列中移除该项(当数列被引用通过时)。在函数式编程中,目标之一是减轻副作用。所以在后台用 first 和 last 函数将数列复制一份,这样原始数列就永远不会被更改了。与之相对应的是 rest 和 but-last 函数,我们可以继续使用 array_slice 来返回该部分。
/**
* Returns the first item in an array.
*/
function first(array $arr) {
$copy = array_slice($arr, 0, 1, true);
return array_shift($copy);
}
/**
* Returns the last item in an array.
*/
function last(array $arr) {
$copy = array_slice($arr, 0, NULL, true);
return array_pop($copy);
}
/**
* Returns all but the first item in an array.
*/
function rest(array $arr) {
return array_slice($arr, 1, NULL, true);
}
/**
* Returns all but the last item in an array.
*/
function but_last(array $arr) {
return array_slice($arr, 0, -1, true);
}
当然,这些都只是低阶函数,可能看起来并不那么让人兴奋,但它们迟早会有用。顺便问一下,大家知道 PHP 中与这些函数相对应的「应用」( https://en.wikipedia.org/wiki/Apply)吗?答案可能是否定的。因为它们的名字十分深奥,不像其它编程语言中那些概念相同但名称普通的命令。让我们继续将 call_user_func_array 替换为 apply 函数吧。
/**
* Alias call_user_func_array to apply.
*/
function apply(callable $callback, array $args) {
return call_user_func_array($callback, $args);
}
这太让人兴奋了!当我们将函数名称变得地道,并创建出低级别的抽象名称,便有了一个能帮助我们创建更多有趣名称的平台。让我们用 apply 帮助我们创建 complement:
function complement(callable $f) {
return function() use ($f) {
$args = func_get_args();
return !apply($f, $args);
};
}
这里使用了 func_get_args()函数,当所有值通过原始函数时,它就能够抓取一个数列,这一数列中所有的值都按照它们通过时的顺序排列。我们继续返回匿名函数,该函数能通过 use 获取原始函数 $f(因为所有的函数在PHP中都有新的域),然后在 $args 中调用 apply。
太好了,现在我们有了 complement 函数,它能让我们更加容易地实施与filter 函数相反的 remove 函数。通过返回回调的 complement 传递给filter,当所有数据与预设条件不相符时,返回所有数据。
/**
* Return a new array with elements for which predicate returns false.
*/
function remove(callable $callback, array $arr, $flag=0) {
return filter(complement($callback), $arr, $flag);
}
换个角度来说,array_merge 和 contact 是等效的。下面以 Cons 和 conj 为例,在 Clojure 中,它们是向集合的开始或末尾增加项的标准方式,
/**
* Alias array_merge to concat.
*/
function concat() {
$arrs = func_get_args();
return apply('array_merge', $arrs);
}
/**
* cons(truct)
* Returns a new array where x is the first element and $arr is the rest.
*/
function cons($x, array $arr) {
return concat(array($x), $arr);
}
/**
* conj(oin)
* Returns a new arr with the xs added.
* @param $arr
* @param & xs add'l args to be added to $arr.
*/
function conj() {
$args = func_get_args();
$arr = first($args);
return concat($arr, rest($args));
}
例如,现在调用这两个函数,会生成相同的结果:
cons(1, array(2, 3, 4));
conj(array(1), 2, 3, 4);
这些低阶工具足以让 interleave 的书写变得十分简单。首先,我们使用func_get_args,取代在函数签名中使用声明参数,这样便能采用大量的数列作为函数参数。然后,我们将每个数列的第一项提出来组成一个新的数列,余下的每个数列作为每一个新数列。接着,检查每个数列是否都保留有元素,再使用 concat 函数连接交错数列的结果,如此反复。以可读的实施以及与 Clojure 版本几乎无差别的函数结果为结束,得到的结果就是证明 Clojure 生成了惰性序列。
/**
* Returns a sequence of the first item in each collection then the second, etc.
*/
function interleave() {
$arrs = func_get_args();
$firsts = map('first', $arrs);
$rests = map('rest', $arrs);
if (every(function($a) { return !empty($a); }, $rests)) {
return concat($firsts, apply('interleave', $rests));
}
return $firsts;
}
因此,当我们调用长度可变的数列来制作函数时:
interleave([1, 2, 3, 4], ["a", "b", "c", "d", "e"], ["w", "x", "y", "z"])
插入所有三个数列减去多余项,以其作为结果数列并以此结尾:
array (
0 => 1,
1 => 'a',
2 => 'w',
3 => 2,
4 => 'b',
5 => 'x',
6 => 3,
7 => 'c',
8 => 'y',
9 => 4,
10 => 'd',
11 => 'z',
)
当然,Clojure 有非常棒的功能性,在这里我们并没有提到,例如 interleave,它是返回惰性序列,而不是静态采集。此外,由于数列会像 PHP 中的映射一样加倍,那些类似于 assoc 的模拟方法就变得模棱两可。如果大家对这些感兴趣,并且想在下一个项目中使用它们,这些代码已放到 GitHub 上供您阅读参考。
原文地址:http://blackwood.io/porting-clojure-php-better-functional-programming/
本文系 OneAPM 工程师编译整理。OneAPM 是应用性能管理领域的新兴领军企业,能帮助企业用户和开发者轻松实现:缓慢的程序代码和 SQL 语句的实时抓取。想阅读更多技术文章,请访问 OneAPM 官方博客。
本文转自 OneAPM 官方博客
优化函数式编程:向 PHP 移植 Clojure 函数的更多相关文章
- Python学习札记(二十) 函数式编程1 介绍 高阶函数介绍
参考: 函数式编程 高阶函数 Note A.函数式编程(Functional Programming)介绍 1.函数是Python内建支持的一种封装,我们通过一层一层的函数调用把复杂任务分解成简单的任 ...
- C#中的函数式编程:递归与纯函数(二) 学习ASP.NET Core Razor 编程系列四——Asp.Net Core Razor列表模板页面
C#中的函数式编程:递归与纯函数(二) 在序言中,我们提到函数式编程的两大特征:无副作用.函数是第一公民.现在,我们先来深入第一个特征:无副作用. 无副作用是通过引用透明(Referential ...
- day03 函数基本语法及特性 2. 参数与局部变量 3. 返回值 嵌套函数 4.递归 5.匿名函数 6.函数式编程介绍 7.高阶函数 8.内置函数
本节内容 1. 函数基本语法及特性 2. 参数与局部变量 3. 返回值 嵌套函数 4.递归 5.匿名函数 6.函数式编程介绍 7.高阶函数 8.内置函数 温故知新 1. 集合 主要作用: 去重 关系测 ...
- Python函数式编程:内置filter函数使用说明
filter操作是函数式编程中对集合的重要操作之一,其作用是从原集合中筛选符合条件的条目,组成一个新的集合. 这在我们日常编程中是非常常见的操作.我们通常的做法是通过循环语句来处理. 而使用filte ...
- C#中的函数式编程:递归与纯函数(二)
在序言中,我们提到函数式编程的两大特征:无副作用.函数是第一公民.现在,我们先来深入第一个特征:无副作用. 无副作用是通过引用透明(Referential transparency)来定义的.如果一个 ...
- [学习] 从 函数式编程 到 lambda演算 到 函数的本质 到 组合子逻辑
函数式编程 阮一峰 <函数式编程初探>,阮一峰是<黑客与画家>的译者. wiki <函数编程语言> 一本好书,<计算机程序的构造与解释>有讲到schem ...
- Python学习笔记八:文件操作(续),文件编码与解码,函数,递归,函数式编程介绍,高阶函数
文件操作(续) 获得文件句柄位置,f.tell(),从0开始,按字符数计数 f.read(5),读取5个字符 返回文件句柄到某位置,f.seek(0) 文件在编辑过程中改变编码,f.detech() ...
- go 学习笔记之学习函数式编程前不要忘了函数基础
在编程世界中向来就没有一家独大的编程风格,至少目前还是百家争鸣的春秋战国,除了众所周知的面向对象编程还有日渐流行的函数式编程,当然这也是本系列文章的重点. 越来越多的主流语言在设计的时候几乎无一例外都 ...
- Python---12函数式编程------12.1高阶函数
函数式编程 函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计.函数就是面向过程的程序设计 ...
随机推荐
- linux配置学习笔记(一):如何提高ssh连接的速度
服务器端sshd配置文件 /etc/ssh/sshd_config 看是否有如下的两条配置条目 GSSAPIAuthentication no UseDNS no 如果前面带#,请把#删掉,或者新添加 ...
- php正则失效-最大回溯(pcre.backtrack_limit)/递归限制
有时候,我们觉得,没有什么可以让我们快乐,我们甚至忘记了如何微笑.但是,当我们被一群乐观.欢乐的人包围的时候,他们从内心深处散发出来的欢迎一定会感染你. 这组照片中,你会看到真正的幸福和快乐的面孔,我 ...
- JavaScript高级程序设计(九):基本概念----函数
一.参数的理解 1.ECMAScript中的参数在内部是用一个数组来表示的.函数接收到的始终是这个数组,而不关心数组中包含多少个参数,即使没有参数也可以. 2.实质上,函数可以通过arguments对 ...
- dubbo监控活跃线程数
telnet对应dubbo服务的ip+端口号 status -l 其中的active就是当前的活跃线程数 通过程序定时探测写入DB,再查询渲染出来就好了 监控报警,如果已经有监控平台,可以通过一定的规 ...
- wiegand/韦根
韦根 参考: 1.wiegand/韦根驱动
- mysql innodb 数据打捞(四)innodb 簇不连续页扫描提取(试验)
一,用winhex把正常页有意做成不连续的两部分,把后8K向后移动4K,中间隔开4K,启动第一次扫描; 扫描结果是,没有提取到有效页面,但在输出目录生成两个文件:upper.pages和upper.l ...
- 九度OJ 1024 畅通工程 -- 并查集、贪心算法(最小生成树)
题目地址:http://ac.jobdu.com/problem.php?pid=1024 题目描述: 省政府"畅通工程"的目标是使全省任何两个村庄间都可以实现公路交通(但 ...
- 模板:函数memset
需要的头文件 <memory.h> or <string.h> memset 函数介绍 void *memset(void *s, int ch, size_t n); 函 ...
- OpenJudge 2737 大整数除法
链接地址:http://bailian.openjudge.cn/practice/2737/ 题目: 总时间限制: 1000ms 内存限制: 65536kB 描述 求2个大的正整数相除的商 输入 第 ...
- mysql---索引及explain的作用
索引:是一种数据结构,以增加存储开销和减慢DML(增.删.改)操作来提高查询速度. 常见的索引结构:btree索引(myisam,innodb,memory,heap),hash索引(memory,h ...