笔者在今年冬天开始时,在 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 获取原始函数 (因为所有的函数在中都有新的域),然后在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 上供您阅读参考。cljphp on GitHub
原文地址:http://blackwood.io/porting-clojure-php-better-functional-programming/
本文系 OneAPM 工程师编译整理。OneAPM 是应用性能管理领域的新兴领军企业,能帮助企业用户和开发者轻松实现:缓慢的程序代码和 SQL 语句的实时抓取。想阅读更多技术文章,请访问 OneAPM 官方博客。
本文转自 OneAPM 官方博客
最佳答案