文档章节

优化函数式编程:向 PHP 移植 Clojure 函数

OneAPM蓝海讯通
 OneAPM蓝海讯通
发布于 2016/03/01 14:04
字数 2268
阅读 30
收藏 0

许多通用程序设计语言试图兼容大多数编程范式,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 官方博客

本文转自 OneAPM 官方博客

© 著作权归作者所有

OneAPM蓝海讯通
粉丝 94
博文 631
码字总数 1266889
作品 0
海淀
私信 提问
【PDF分享】函数式编程之 Clojure

之前我们发过来自搜狐 @特价萝卜 在公司内部做的技术分享的讲稿的 PDF —— Clojure 简介与应用。 而这份 PDF 是根据之前做的技术分享《Clojure简介与应用》的讲稿改编了一下搞成了一篇博客《...

oschina
2012/08/20
3.9K
3
Clojure 语言的设计

via:AVOS Cloud Blog 基于 JVM 的决定 Clojure 能够吸引人的很重要一点是它是 JVM 之上的语言,这个决定非常关键。 首先,因为根植于 JVM 之上,并且做到了跟 Java 语言的相互调用,它能吸引...

都比zunxi
2014/08/21
593
0
Scala与Clojure函数式编程语言青春焕发

在StackOverflow发布的2015开发者调查,最受程序员喜爱的开发语言中,Clojure和Scala分别位列第五和第六。排名第一的是 Swift,而Swift相比原先的Objective-C最重要的优点之一,就是对函数式...

图灵教育
2015/05/21
417
0
Clojure 哲学

简单性、专心编程不受打扰(freedom to focus)、给力(empowerment)、一致性和明确性:Closure编程语言中几乎每一个元素的设计思想都是为了促成这些目标的实现。 学习一门新的编程语言往往...

oschina
2013/05/01
6K
17
选择正确的开源编程语言

要选择编程语言通常很困难,因为有非常多的选项。 在这篇文章中,试图学习编码的新手可以看到顶尖编程语言的有关概述,文中分析了编码的简洁性和复杂性,并权衡每种语言的利弊。 技术界每年都...

oschina
2016/11/04
5.3K
18

没有更多内容

加载失败,请刷新页面

加载更多

500行代码,教你用python写个微信飞机大战

这几天在重温微信小游戏的飞机大战,玩着玩着就在思考人生了,这飞机大战怎么就可以做的那么好,操作简单,简单上手。 帮助蹲厕族、YP族、饭圈女孩在无聊之余可以有一样东西让他们振作起来!...

上海小胖
31分钟前
5
0
关于AsyncTask的onPostExcute方法是否会在Activity重建过程中调用的问题

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://blog.csdn.net/XG1057415595/article/details/86774575 假设下面一种情况...

shzwork
今天
7
0
object 类中有哪些方法?

getClass(): 获取运行时类的对象 equals():判断其他对象是否与此对象相等 hashcode():返回该对象的哈希码值 toString():返回该对象的字符串表示 clone(): 创建并返此对象的一个副本 wait...

happywe
今天
6
0
Docker容器实战(七) - 容器中进程视野下的文件系统

前两文中,讲了Linux容器最基础的两种技术 Namespace 作用是“隔离”,它让应用进程只能看到该Namespace内的“世界” Cgroups 作用是“限制”,它给这个“世界”围上了一圈看不见的墙 这么一...

JavaEdge
今天
8
0
文件访问和共享的方法介绍

在上一篇文章中,你了解到文件有三个不同的权限集。拥有该文件的用户有一个集合,拥有该文件的组的成员有一个集合,然后最终一个集合适用于其他所有人。在长列表(ls -l)中这些权限使用符号...

老孟的Linux私房菜
今天
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部