内容


利用 PHP 5.3 的 lambdas 和 closures

使用 lambdas 和 closures 成为一个更优秀的开发人员

Comments

在过去的几年中,围绕着编程语言开展了很多活动。开发人员已经学习了 Ruby、Groovy 和 Clojure 这类语言 — 不仅是拓宽其销路,而且还拓宽其思维。这些语言的两个最精彩的特性已在 PHP 5.3 中提供:lambdas 和 closures。还有大多数 PHP 开发人员已经采用的其他语言、具有 lambdas 和 closures 的完美实现:JavaScript。

通过增加 lambdas 和 closures,PHP 满足了编程语言的功能需求。特别地,函数式编程语言必须提供高阶和一流的函数,这是本文所定义的。在过去的几年中,人们对函数式编程语言的兴趣高涨。原因是什么?很简单:摩尔定律(指出计算能力每两年会翻一番)不再适用。它在某些方面不正确,因为单核处理器的计算能力已经提高,计算机厂家现在仅能提供通过创建多核处理器来提高性能。这就是我们已经看到双核、四核、和六核处理器的原因。对于计算机厂商来说,制造多核处理器已经变得相对简单,但有状态、基于堆叠的编程语言无法从附加的处理器获益。函数式编程语言则能因此而受益。

什么是 lambdas 和 closures,以及为何 PHP 开发人员会因为这些功能而兴奋呢?本文描述了如何以及在哪里使用 lambdas 和 closures,向您展现 closures 的一些行为特点,并向您介绍一些有助于充分利用 lambdas 和 closures 的优势的代码惯用语法。

Lambdas:快速而简单

术语 lambda 来自 Lambda calculus,这一在 20 世纪 30 年代由 Alonzo Church 提出的,关于函数定义、应用程序、递归的规范系统。关于 Church,一件很有趣的事是,他在 Stanford University 的同事之一是著名的图灵机的发明者 Alan Turing。这两个人所提出的理念成为了现代编程语言的基础。

但是,不要让 calculus 的复杂性妨碍了您对 lambdas 的使用:它们也只不过是匿名函数。例如,以下的函数定义就是 lambda:

$horse = function() {return "with no name";};
print $horse();

注意,需要用分号(;)来结束,函数正被分配给一个变量。在 PHP 5.3 之前,您必须已经创建了标准函数:

function horse() {return "Secretariat";}
print horse();

“妙极了!” 您会这样说。“这两个例子行数相同,除了 — 无名函数基本上会在变量 $horse 中有了名字。” 事实上,$horse 是对一个函数的引用,而不是函数的名字。但是 lambdas 的真正优势是,在您忙着创建函数时,不需要变量引用。例如:

$a=array('PA'=>"Pennsylvania",'VA'=>"Virginia",'TX'=>"Texas");
print_r(array_filter($a,
                     function($v){return $v==="Virginia";}
                    )
);

在本例中,lambda 函数保持没有名字:它是在内侧作为 array_filter 函数的参数来创建的。在运行中,作为函数和方法的参数来创建 lambdas,这是 lambda 的主要使用示例。注意,术语 callback 通常用于描述作为参数传递给其他函数的函数。在事件处理架构中经常采用回调,这样您指定的代码将在稍后调用。

在函数式编程中,lambdas 被称为一流的函数 因为它们能够:

  • 作为参数来传递。
  • 可在运行过程中创建并使用。
  • 从函数返回。
  • 分配给变量。

事实上,自 V4.0 起,lambdas 以通过 create_function 来向 PHP 开发人员提供:

$horse = create_function('', 'return "with no name";');
print $horse();

但是 create_function 的语法很繁琐。一方面,该代码是在一个大引证字符串中。因此,不要期望从 PHP 编辑器获得帮助,而且不要忘了尽量避免使用该代码串。此外,create_function 的代码是在执行时进行编译,而且结果函数无法被缓存。

lambdas 声明

此处有一个关于 lambda 使用的简单示例 — 文中会多次使用该示例。应用程序的需求是:当我读取包含代码片段的 PHP 文章时,它们通常展示具有 echoprint 的结果,后面还有描述所期望输出的注释。比如,考虑如下代码:

function horse() {return "Secretariat";}
print horse(); // should list: Secretariat

读者需要运行代码来可视化地验证隐含的假设。当我读取:

print horse(); // should list: Secretariat

我觉得在代码和注释之间看来看去搞得我很晕。为使本文中代码段的可读性更强,我采用 PHP asserts。例如:

assert(horse() == "Secretariat");

PHP assertions 的配置,使得您可以在不管是否已调用 assert 的情况下,指定要调用的函数。当然,我使用了 lambda:

assert_options(ASSERT_CALLBACK, 
	function ($script, $line) {
		echo "assert failed file:$script line:$line ";
        }
);

当 assertion 失败时,将会输出脚本名和行数:

assert failed file:/var/www/closure_behavior.php line:13 PHP Warning: 
assert(): Assertion failed in /var/www/closure_behavior.php on line 13

在 assertion 中对 lambda 使用的替换可能是:

function assertCallback($script, $line) {
	echo "assert failed file:$script line:$line ";}
);
assert_options(ASSERT_CALLBACK, assertCallback);

您可能会问,“为什么 lambda 版本更好?” 让我来解释一下。请记住,非 lambda 版本定义的函数仅用于一个地方。要理解对代码的读取操作要远比写入操作多,因此简化代码很重要。在您定义有名称的函数时,您的读者会企图将函数名输入到他/她的记忆中,这没有必要复杂化读者对程序的理解。如果函数是一劳永逸的操作,可采用 lambda 并简化您的代码。

纵观本文的其余部分,当见到 assert 时,您会肯定 assertion 是正确的。

以 closures 开始

如果 lambdas 是没有名字的函数,那么 closures 仅比 lambdas 多一个上下文。就是说,closure 是个匿名函数,在其创建时,将来自创建该函数的代码范围内得变量值附加到它本身。变量通过 use 关键字绑定到 closure。比如,如果有以下变量:

$states=array('PA'=>"Pennsylvania",'VA'=>"Virginia",'TX'=>"Texas");
$st = 'Texas';

您可利用 use 关键字将 $st 变量绑定到 closure,示例如下:

$tx = array_filter($states, function($v) use($st) {return $v==$st;});

然后 assert 成功:

assert(array_keys($tx) == array('TX'));

正如上面例子说明的,closure 是 “closed over” 特定变量的函数。以上示例是 closures 很好的使用例子:它采用 18 个标准 PHP 阵列函数之一,它将函数作为参数(比如 array_walkusort、和 array_app)。Closures 允许您扩展使用那些实用函数。在 closures 之前,对所传递函数的实施限定在其函数参数的上下文。(事实上,这并不完全正确,因为您可以有使用过的全局。但是,传统观点是全局使用是一个糟糕的编程实践。)

PHP 的 array_filter 是函数式编程语言中高阶函数 的一个例子。高阶函数(同时在数学和计算机科学中)将函数作为其他函数的输入或输出。在适应了 lambdas 和 closures 以后,您可能会发现,通过编写您自己的高阶函数您已经减少了代码并创建了更多的可读与可用代码。

closure 事实上是与新的 Closure 类共同在内部实施的新数据类型。从另一个角度来看:如果 object 是带有在其上运行的方法的数据,那么 closure 是绑定了数据的函数。

Closure 行为

可以很容易地理解 closure 的 use 关键字的语法。但是在您开始理解通过 use 关键字传递给变量的行为之前,需要熟悉一下 closures。我相信您对变量范围有着清楚的理解:编写良好的代码仅在所要求的范围内定义变量。考虑到这一点,您可能会在以下代码失败的之后,期望断言:

function getNameFunc() {
     $string = 'Denoncourt';
     return function() use ($string) { return $string; };
}
$name = getNameFunc();
$name();
assert ($name() == 'Denoncourt');

但是即使 $string 超出了范围,断言还是成功了。这是因为在 closure 的关键字 use 中指定的变量在词法上确定范围。让我来解释一下:PHP 变量通常是 动态确定范围的,这意味着变量放置在堆栈中。当其所包含的函数完成后,这些变量将从堆栈中弹出。在词法上确定范围 是指在本地环境中定义变量。而不是放置到堆栈中,变量通过 use 关键字指定,并存储到静态存储中。

这是 closures 的强大功能 — 不管它们是在哪里定义的 — 它们可在以后从任何地方调用,并与其使用指定的,仍然位于有效范围内的变量一起执行。然而,请注意,词法范围的 PHP 实施将在 use 关键字中指定的变量复制到静态存储区域中。当使用大对象时,您可通过将引用传递给变量,来降低存储,如下:

$string = 'Denoncourt';
$changeName = function() use(&$string) {$string = 'Smith';};
$changeName();
assert ($string == 'Smith');

上面的例子还显示出,通过引用传递使用变量是允许 closure 修改变量的方法之一。

“等一下,” 您会说,“为什么不仅仅将传递到 closure 的 use 选项中的变量作为函数参数来传递?” 在词法上确定范围的变量就是在此处发挥作用。带有函数参数,参数传递必须在堆栈范围内进行。但是,通过使用变量,closure 基本上记住了变量的上下文。考虑清单 1 中的示例,其中 closure 定义为类,并且在对象超出范围之外后进行调用。

清单 1. 在对象超出范围之外后调用 closure
class Person {
	var $first;
	var $last;
	public function sayHello() {
		$that = $this;
		return function() use ($that) { 
			return $that->first.' '.$that->last; 
        	};
	}
}
function stackFunc1() {
	$me = new Person();
	$me->first = 'Don';
	$me->last = 'Denoncourt';
	return $me->sayHello();
}
$sayHi1 = stackFunc1(); 
function stackFunc2() {
	$me = new Person();
	$me->first = 'Sue';
	$me->last = 'Swartzbaugh';
	return $me->sayHello();
}
$sayHi2 = stackFunc2(); 
assert($sayHi1() == 'Don Denoncourt');
assert($sayHi2() == 'Sue Swartzbaugh');

注意,Person::sayHello 方法实施为 $this 特定变量创建别名,并将其传递给 closure 定义。这就是 closure 与 Closure 类共同在内部实施,并且,该类具有自己的 $this 上下文。事实上,如果您想在 use 关键字中传递 $this,您将会得到与此相类似的错误:

PHP Fatal error:  Cannot use $this as lexical variable

用于类方法并返回函数的用例仅利用您的创造力进行限定。关于 closures 的创造性使用的例子是,检查 Pawel Turlejski 的实用类,它提供与 Groovy 类似的阵列方法(见 参加资料)。

通过使用 closures,您甚至可以对您的类进行编程,来允许动态创建方法。清单 2 提供了一个例子。

清单 2. 动态方法创建编码
class Person {
    var $first;
    public function __call($method, $args) {
        return call_user_func_array( $this->$method, $args);
    }
}
$me = new Person();
$me->first = 'Don';
$me->sayGoodbye = function() use ($me) {
    return 'Bye '.$me->first;
};
assert($me->sayGoodbye() == 'Bye Don');

即使对象超出范围,closure 仍可被成功调用。例如,即使您移除引用了 PHP 的 unset 方法的 $me 对象,以上的 closure 执行仍然可以工作:

$bye = $me->sayGoodbye;
unset($me);
assert($bye() == 'Bye Don');

参数传递给高阶函数,然后作为参数传递给 closure 的 use 关键字,仍保持其值:

$greeting = function($greet) {
	return function($name) use($greet) {
		return $greet.' '.$name;
	};
};
$hi=$greeting('Yoh');
assert($hi('Don') == "Yoh Don");
assert($hi('Sue') == "Yoh Sue");

调用类

早些时候,我提到过 closures 和称为 Closure 的类一起在内部实施。(该类,顺便说一句,可能在 PHP 随后的版本中有变化,因此不要在开发中直接使用)。但是,有一个您可在内部实施的 Closure 类中利用的特性:它使用新的 __invoke 方法。__invoke 的神奇方法使得类可调用:

class Invoker {
	public function __invoke() {return __METHOD__;}
}
$obj = new Invoker;
assert ($obj() == 'Invoker::__invoke');

当包含 __invoke 实施的类被实例化了, 您可通过在对象名的后面指定 parentheses (()) 来调用类。 这种类型的对象称为 functor 或者 function object

结束语

可通过采用 lambdas 和 closures 来使您的应用程序获益。通过用匿名函数来替换正式函数定义,您可简化代码。通过将使用函数和方法作为高阶函数进行开发,可使应用程序更加健壮。通过理解 closures 和文本化范围,您可开始感受到出现新的开发模式和惯用语法的机会。并且通过更深入学习函数式编程,您可拓宽编程知识。

是否应当将 PHP 作为函数式编程语言来使用?通常,不是:PHP 已演化为用于开发 web 应用程序的精干的语言。采用纯函数式语义来开发基于 PHP 的 Web 应用程序,违背了该语言的最初动机。在任何情况下,PHP 用于 lambdas 和 closures 语法显得有点不雅(特别是当与 Ruby、Groovy 和 Clojure 之类的语言对比时)。但是,有这样的场景,函数式编程概念应用程序可适用于 PHP 应用程序,其结果是代码变得更简单、更简洁、更易于理解。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=606125
ArticleTitle=利用 PHP 5.3 的 lambdas 和 closures
publish-date=12312010