许多通用程序设计语言试图兼容大多数编程范式,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 上供您阅读参考。
cljphp on GitHub
原文地址: http://blackwood.io/porting-clojure-php-better-functional-programming/
本文系OneAPM 工程师编译整理。OneAPM 是应用性能管理领域的新兴领军企业,能帮助企业用户和开发者轻松实现:缓慢的程序代码和 SQL 语句的实时抓取。想阅读更多技术文章,请访问OneAPM 官方博客。