许多通用程序设计语言试图兼容大多数编程范式,PHP 就属于其中之一。不论你想要成熟的面向对象的程序设计,仍是程序式或函数式编程,PHP 均可以作到。但咱们不由要问,PHP 擅长函数式编程吗?本文系国内 ITOM 管理平台 OneAPM 工程师编译整理。php
笔者在今年冬天开始时,在 Recurse Center致力于学习 Clojure,更加深刻地了解了函数式编程,并从新拾起 PHP 的客户端工做。但笔者仍然但愿运用一些高阶函数和概念,并对它们进行研究。html
笔者已经在 PHP 中实施了模拟 LISP 语言,并看到了一些在 PHP 中经过使用 underscore 类库以兼容某些关键函数方法的尝试。但为了使 Clojure 在写入其它编程语言时仍然保有较高的速度,笔者特地镜像 Clojure 的标准库,以使本身能在编写真正的 PHP 代码时,以 Clojure 的方式思考。虽然在学习的过程当中绕了一些弯路,笔者仍然愿意向各位展现本身是如何实现 interleave 函数的。java
幸运地是,已经有人执行了 array_some 和 array_every,而且很是地道(至少笔者这么认为)。git
/** * 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。github
/** * 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 来返回该部分。app
/** * 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 官方博客