PHP-函数式编程高级教程-全-
PHP 函数式编程高级教程(全)
一、介绍
函数式编程通常不会与 PHP 联系在一起。然而,很长一段时间以来,PHP 已经具备了使用函数范式创建软件的所有必要特性。在本书中,您将了解什么是函数式编程,如何在 PHP 中实现函数式编程,以及使用函数式编程改进 PHP 软件的不同方法。
这本书是给谁的?
这本书本身并不是 PHP 入门;它假设您在 PHP 脚本方面有一些基本的(或者更高级的)经验。你不需要成为一个专家来跟随;我将介绍 PHP 中的所有关键概念,您需要了解这些概念才能在代码中实现功能性设计,并为您指出资源的方向,例如网站和其他书籍,您可以使用它们来学习或研究我没有直接介绍的任何相关概念。
撇开绝对的 PHP 初学者不谈,这本书适合所有程序员。无论您迫切需要学习函数式编程(也许您已经接管了函数式 PHP 代码库),还是您只是对了解函数式编程的“热点”感兴趣,这本书都有适合您的内容。对于那些怀疑使用函数式编程范式创建软件的人来说,甚至可能有所帮助。我认为大多数程序员会从函数式编程风格中找到有用的经验和代码模式,从而增强他们的面向对象或过程式编程工作。如果所有这些都失败了,函数式编程的知识在你的简历上会很好看!
什么是函数式编程?
函数式编程是一种声明式编程范式,它将代码抽象成纯粹的、不可变的、无副作用的函数,允许程序员将这些函数组合在一起,以制作易于推理的程序。
这就是我对函数式编程的定义。让另外五个函数式程序员来定义函数式编程,你会得到四个以上的答案(两个只是从维基百科复制了相同的答案)。没有“标准”的定义;不同的人和不同的编程语言实现函数式编程元素是不同的。这些差异部分是因为所讨论的语言的实用性,有时是因为目标平台、数据和使用场景,但通常它们归结为我所说的“编程信仰”:一种固定的、有时不合理的、但通常是根深蒂固的关于特定范式应该如何的信念。即使在 PHP 函数式程序员的小社区中,你也不会找到一个确切的共识。在 PHP 中,函数式编程不是一个核心概念,但即使在它所在的语言中(例如 Lisp、Scala 等)。),对于什么构成真正的函数式编程有很多“相关”的理解。虽然这听起来可能有问题,但你仍然会“当你看到它时就知道了”,当它变得模糊不清时,你可以选择以任何你认为合适的方式来定义它!
PHP 不是一门纯粹的函数式编程语言,但是你仍然可以用它来进行函数式编程(这很好;不然这本书也不会很长)。一些纯粹主义者认为基本的函数式编程概念中的一些元素很难用 PHP 的标准语法来实现,所以说可以用 PHP 中的函数式编程“风格”来编程可能更准确一些。
现在让我们更深入地看看函数式编程实际上是什么。函数式编程是一种“声明式”编程风格,这意味着您指定您希望它做什么,而不是您希望它如何做。这是比你可能习惯的面向对象或过程编程更高层次的抽象。然而,当使用 SQL、HTML、正则表达式和类似的语言时,您几乎肯定会在日常生活中使用声明式编程。考虑清单 1-1 中显示的 SQL 片段。
SELECT forename,
Surname
FROM users
WHERE username = 'rob'
AND password = 'password1';
Listing 1-1.
declarative.sql
这是告诉您的数据库服务器您希望它做什么(根据超级机密的安全凭证选择真实的名称),但您没有告诉它如何做。你不要告诉它以下内容:
- 在磁盘的什么地方寻找数据
- 如何解析或搜索匹配记录的数据
- 如何确定记录是否符合您的标准
- 如何从记录中提取相关字段
等等。你只需告诉它你希望它为你实现什么。
很明显,在某些时候,你需要告诉计算机如何做某事。对于清单 1-1 中的 SQL 示例,您可以通过让一些相当聪明的人为您编写数据库管理软件(DBMS)来实现。在函数式编程中,您往往需要自己编写实现代码,但是为了使它成为一项可管理的任务,您可以将其分解成尽可能小的块,然后使用声明性函数调用的层次链来告诉计算机如何处理该代码。如果您使用 Composer 依赖管理系统,您将已经在使用一个类似的范例:有许多代码库可以抽象出您需要做的任务;您只需简单地“组合”一个库列表来做您想做的事情。在函数式编程中,你做的完全一样;你用函数做一些事情(像库 Composer 提供的那样),然后把它们组合成一个程序。
有一个基本上是你想要实现的目标的列表的计划在纸上听起来很好,并且确实使它很容易理解和推理你的计划。为了让这个想法更具体一点,让我们看一个小的函数式程序(清单 1-2 )。
<?php
require_once('image_functions.php');
require_once('stats_functions.php');
require_once('data_functions.php');
$csv_data = file_get_contents('my_data.csv');
$chart = make_chart_image (
generate_stats (
data_to_array (
$csv_data
)
)
);
file_put_contents('my_chart.png', $chart);
Listing 1-2.example.php
这显然是一些被抽象成一组函数的代码,这些函数规定了它的功能(根据从读入的一些数据中准备的一些统计数据绘制一个图表)。您可能还会看到,how 隐藏在顶部的必需文件中,但是程序做什么还是很清楚的。如果你的需求改变了,你想打印一个表格而不是绘制一个图表,你可以简单地用print_table()
替换draw_chart()
,很明显会发生什么。这是一个函数式程序的(非常松散的)例子。
这听起来很棒。但是,即使不考虑隐藏在所需文件中的代码,您的程序员直觉可能会告诉您,将随机函数链接在一起,并用一个替换另一个,是一个危险的提议,尤其是当您看不到它们是如何实现的时候。例如,您如何知道read_data()
会以正确的格式返回数据供prepare_stats()
处理?你怎么能确定你可以用prepare_stats()
替换掉draw_chart()
,它仍然会像你期望的那样工作?显然,函数式编程涉及的不仅仅是“用一个描述性的名称将它全部放入一个函数中”,在阅读这本书的过程中,您将看到构造函数的各种方法,这样您就可以将它们作为代码的“小黑盒”来使用,可以轻松可靠地将它们串联起来。
顾名思义,函数式编程是围绕函数进行的。然而,函数式编程意义上的函数与 PHP 语法意义上的函数并不完全相同,尽管您将使用 PHP 的函数实现来实现 FP 函数。函数式编程函数通常被称为纯函数,它有几个重要的特征,可以用 PHP 的语法来模仿,但不是强制的。纯函数具有以下特征:
- 是透明的
- 没有副作用
- 没有外部依赖性
我将在接下来的几章中更详细地讨论这些特性的含义,但它们可以归结为一个函数,即一个小型的自包含“黑盒”,它接受明确定义的输入,产生明确定义的输出,给定相同的输入总是产生相同的输出。特别是,函数只作用于给定的输入(它不考虑任何外部状态或数据,只依赖于调用它的参数),它唯一的作用是返回一些输出(每次你给它相同的输入,输出都是相同的);因此,它不会改变程序或系统外部的状态。
在清单 1-2 中的示例程序中,如果您每次都给read_data()
一个完全相同的 CSV 文件,并且假设您已经正确地使用了函数式编程,那么您可以确定draw_chart()
每次都会产生完全相同的图表,而不管程序中的其他地方或您的系统上发生了什么。这种确定和推理程序流的能力为函数式编程范式带来了许多好处,随着您对实现函数式编程程序的了解越来越多,您将会发现这些好处。
函数式编程是可靠的
如果您来自面向对象编程背景,您可能熟悉编写 OO 代码的坚实原则。虽然是为 OOP 写的,但是扎实的原理实际上很好的体现了函数式编程的原理;事实上,有些是函数式编程定义方式所固有的。理解这些原则将有助于你理解函数式编程的一些关键特征。如果你不熟悉 SOLID,它是一个缩写词,每个字母代表优秀 OO 设计的五个基本原则之一。我将逐一介绍。
单一责任原则
每个功能只负责一项任务。“做一件事,而且要做好。”在函数式编程中,你将程序分解成单任务函数。当任何其他函数需要完成该任务时,它们调用该函数。并且当该任务的规格发生变化时,只有该功能必须改变。
o:开放/封闭原则(OCP)
功能应该“对扩展开放,对修改关闭”在函数式编程中,这意味着当您需要扩展一个函数的行为时,您不需要修改现有的函数,而是将它与创建新的扩展功能的其他函数组合在一起(或从其他函数调用它)。
李斯科夫替代原理
在面向对象的程序设计中,这个原则处理用一个对象的子类型的实例替换该对象的能力。在函数式编程中,这最精确地映射到一个叫做函子逆变的概念上,这个概念比你在本书中学到的更高级(理论性更强,实用性更弱)。但是,您将会看到一个类似的相关原则,即引用透明性。这基本上意味着,不管程序中发生了什么,给定的函数(带有给定的输入)可以用它在程序中返回的值来替换,程序将继续按预期运行。
I:接口隔离原则(ISP)
这个原则意味着调用一个函数所需要的参数应该仅仅是你正在执行的任务所需要的参数。函数的接口应该尽可能具体地分解,而不是提供包含与调用客户端代码的意图无关的参数的通用接口。由于高效组合所需的结构化接口,函数式编程在分解成单个责任函数时几乎默认实现了这一点。
依赖倒置原则(DIP)
这项原则声明如下:
- 高层模块不应该依赖低层模块。两者都应该依赖于抽象。
- 抽象不应该依赖于细节。细节应该依赖于抽象。
这或多或少就是声明式编程的定义。您的具有实现细节的低级函数形成了一个抽象,您的高级“声明性”函数可以在其上操作。
如果所有这些原则和术语现在都没有意义,不要担心;当你阅读这本书的时候,只要记住它们(或者在需要的时候参考它们),到最后你应该能够看到这些坚实的原则是如何自然地应用于函数式编程范例的。
进一步阅读
函数式编程有什么好处?
我已经谈到了一些好处。但是如果还不清楚的话,这些是大多数程序员从使用函数式编程技术中获得的主要好处:
- 您将创建易于推理的代码。这意味着它不容易出错,更容易更新,更容易理解代码流。
- 您将创建更易于测试的代码。这是函数式编程范式的一些属性的副作用,例如不变性、引用透明性和缺乏外部依赖性。
- 您可以使用延迟评估、并行化和缓存等技术来创建更高性能的代码。
谁使用函数式编程,为什么?
大约 20 多年前,当我在大学攻读计算机科学学位时,我第一次学习了函数式编程。当时我真的没有看到它的意义,并且因为我对学习它缺乏兴趣而在那门课上得了一个很差的分数,因为我不明白为什么有人会想在“现实生活”中使用它。当时,除了学术界和某些高度专业化的领域,如工程和金融,它没有被广泛使用。然而,随着时间的推移,这种情况发生了变化,现在使用函数式编程的人比你想象的要多,原因多种多样。
如今,它在主流 It 的许多领域都很突出,而不仅仅是在专门的行业、学术界和由“硅谷潮人”经营的初创公司!网飞、LinkedIn 和 Twitter 等大公司使用函数式编程来大规模交付健壮的服务。有些人用它是因为它很容易推理。有些人使用它是因为它没有“副作用”,易于并行化和扩展。有些人使用它是因为它巧妙地映射到了在工程、数据科学和金融等不同领域中使用的数学结构。它正在一些行业中使用,因为它的声明性允许创建特定于领域的语言(DSL)。DSL 允许不太熟练的用户创建或理解程序,允许更容易地将业务需求和过程映射到软件中,并通过提供特定于任务的抽象来提高程序员的生产率。
在过去的几年里,函数式编程已经获得了越来越多的关注。像 rack、Clojure 和 F#这样的现代函数式语言正在构建 steam,甚至 PHP 也加入了进来,GitHub 和 Composer 上的函数式编程库越来越多。
虽然函数式编程目前是现代计算领域的热门话题,但它在计算领域的历史可以追溯到 20 世纪 50 年代的 Lisp,而 lambda 演算(函数式编程所基于的数学抽象)和更抽象的组合逻辑领域则始于 20 世纪 20 年代和 30 年代。无论是直接在 Lisp 和 Scheme 这样的语言中,还是间接在大多数编程语言中编写的函数式代码中,函数式编程多年来已经对许多计算领域产生了影响。很多公司写函数式编程代码,日复一日的依赖函数式编程软件(不管他们有没有意识到!).
函数式编程是“要么全有,要么全无”吗?
当人们开始学习函数式编程时,他们经常会问自己是否需要撕掉旧的 OOP 代码,从头开始。我的答案是否定的(尽管纯粹主义者会说是)。函数式编程可以愉快地与 OOP 或过程式代码共存,或者实际上与任何其他编程范式共存,只要您了解函数式编程代码的开始和结束位置。由于函数式编程的本质,将函数式编程结构放在对象或传统函数/过程代码块中通常更容易,而不是反过来。
可以从函数式编程中受益的特定代码部分(例如业务逻辑流程和高性能算法)通常是将函数式编程引入代码库的良好起点。因为函数式编程不是 PHP 的核心特性,所以它实际上更容易按照你认为合适的方式混合和匹配范例。在其他传统的函数式编程语言中,事情有更多的限制;例如,Haskell 没有任何面向对象的能力,而 F#是围绕特定的 FP-OO 混合范式编写的。函数式编程与 OOP 有许多共同之处;事实上,闭包(与相关程序状态打包在一起的函数)有点类似于流线型对象,许多 OO 编程模式中的程序流控制有点模仿流的函数式编程风格。和往常一样,PHP 很灵活,它说“做你想做的”,这既是一种祝福,也是一种诅咒(如果你做得不对)。不过,这是对 PHP 的一个普遍抱怨,只要始终正确编程就能简单解决!
如果您有兴趣了解人们多年来提出的不同编程范例之间的差异,那么 Wikipedia 已经为您提供了每种范例主要特性的全面比较。请注意,PHP 是一种“图灵完全”语言,这基本上意味着如果某个东西可以计算,那么 PHP 就可以计算它。这也意味着所描述的任何范例都可以在 PHP 中实现(您是否愿意是另一回事,尽管我认为大多数范例至少提供了一些好的程序员可以在适当的时候添加到他们的心理工具箱中使用的东西)。如果这本书激起了你用不同方式做事的欲望,那么也许你可以查看维基百科的列表,找到更多混合代码的方法。
进一步阅读
为什么要用 PHP 进行函数式编程?
差不多每个听过这本书书名的人都跟我说过“你不用 PHP 做函数式编程”(除了我老婆,她说“那听起来超级无聊”)。嗯,他们都错了(包括我老婆)。绝对没有理由不使用 PHP(除了“为什么不使用 PHP 进行函数式编程”一节中列出的原因)。而且也不是超级无聊。
说真的,PHP 拥有编写好的函数式编程代码所需的一切。它有一流的函数支持,有帮助抽象样板代码的库,并且有越来越多的可用文档和帮助(包括您现在正在阅读的这本书)。但最重要的是,你已经知道了 PHP。你正在学习一种新的编程范式,那么为什么要同时让自己承担掌握一门新语言的负担呢?事实上,如果您首先在已经熟悉的环境中了解这种范式,您可能会发现以后更容易掌握更专业的函数式编程语言,如 Haskell,其中的语言特性隐含地依赖于您对函数式编程范式的熟悉。
也许如果你对编程一点都不熟悉,而想学习函数式编程,从 Haskell 这样的东西开始可能就可以了。但是你带着熟悉面向对象或过程编程的包袱而来,一种纯粹的函数式编程语言对你来说会感到陌生,直到你对函数式编程这个概念感到舒服为止。你将会浪费大量的时间去尝试实现函数式编程世界中不存在的特性和结构,而不是去写代码。
为什么不使用 PHP 进行函数式编程
有一两种情况下,以这种方式使用 PHP 可能不是一个好主意(只是不要告诉任何人我这么说)。
- 您有一个特殊的业务案例/需求,只使用函数式编程。在 PHP 中,没有什么可以阻止你混合编程范式,有时这样做很诱人(或者很实用)。因此,如果你需要确保你坚持使用函数式编程代码,那么最好不要使用 PHP。
- 你需要一流的商业支持。如前所述,PHP 社区中对函数式编程的支持越来越多,质量越来越高。但是“传统的”函数式编程语言有更多,特别是在付费商业支持领域。
- 你认为 PHP 正在消亡,有严重的问题,或者不是一种真正的编程语言。一些人仍然持有这些观点,即使 PHP 越来越强大。然而,如果提到 PHP 就让你感到刺痛,那么在 PHP 中用函数式编程实现你的程序不会神奇地让这种感觉消失。当你在读这本书的时候,更有可能的是你并没有那样的感觉,而是你周围的人(你的老板或者同事?)可能,尽管这本书很精彩,但它不太可能动摇他们对 PHP 本身的看法。在这种情况下,你可能需要做一些务实的事情——辞职,像我一样创办自己的公司。
- 你的同事不懂函数式编程,所以你的代码对他们来说可能更难维护。这很容易解决:给他们买一本这本书!
PHP 版本
在撰写本文时,PHP 当前的稳定版本是 7.1.2,如果这是每个人都在运行的版本,那就太好了。然而,事实并非如此;事实上,从 5.2 开始的 PHP 版本可以在主机提供商和公司内部的主流应用中找到。人们使用更旧版本的故事在互联网的黑暗角落比比皆是,由于安全更新仅适用于 5.6 及更高版本,这样的故事是噩梦的素材。那么,对于函数式编程,你应该选择哪个版本呢?
本书中的所有代码都是在 PHP 7.1 的当前版本上开发和测试的。然而,从 5.4 开始,大部分代码只需稍加修改就可以在任何版本上运行。早于 5.4 的版本缺乏对匿名函数和闭包的语言支持,这意味着函数式编程不实用。我将在整本书中指出版本支持之间的任何差异,其中最值得注意的是 series 5 版本中缺乏对标量变量的类型声明(也称为类型提示)的支持。请注意,对上一个 5.x 版本(5.6)的“积极支持”现已结束,“仅安全修复”支持将于 2018 年底撤销,因此如果可能,强烈建议使用 7.x 版本。以及 PHP 维护人员的持续支持,您会发现 PHP 7 较低的资源需求将有助于 PHP 函数式编程的某些领域,如递归。也就是说,您可以在 PHP 5 中进行函数式编程,许多工作场所中不可避免的升级延迟可能意味着您没有选择使用 5.x 版本,因此我将尽最大努力来涵盖这些差异。
结论
在这一章中,你开始了解什么是函数式编程,以及为什么它在你的 PHP 开发中有用。从这种抽象的讨论中很难理解函数式编程,所以如果您还没有“理解”它,请不要担心。当您阅读接下来的几章时,您会对它有更好的感觉,因为您看到了它背后的编程概念和函数代码的例子。
二、函数式编程关键概念
在这一章中,你将会看到在开始实际的函数式编程之前你需要理解的关键概念、构建模块和词汇。尽管您可能已经在日常编程中使用了函数和闭包,但还是有必要花时间阅读下面的章节,这些章节从基本原则上描述了它们,因为在面向对象或简单的过程式编程中使用它们时,您认为理所当然的一些细节可能会在函数范式中应用它们时出错。因为函数式编程植根于数学,所以你也可以看看一些你可能不熟悉的语言,并用普通程序员容易理解的术语来理解它。
本章介绍的概念,单独来看,可能会描绘出一幅关于什么是函数式编程以及它能带来什么好处的混乱画面。例如,我将讨论不变性,本质上是不能改变一个值。乍一看,这似乎是一个缺点,而不是一个优点,但是当您在接下来的几章中将所有这些概念结合在一起时,您将会看到不变性在函数式编程灵活的类似菜谱的特性中起着关键的作用,并且是允许您轻松地对函数式代码进行推理的因素之一。
所以,现在,试着把注意力集中在理解提出的单个概念上,不要太担心它们是如何组合在一起的。学习函数式编程很像编写函数式编程代码——许多独立的小函数/想法组合成一个总体方案,最终完成一些事情!
检查状态
当你阅读这本书的时候,特别是当你看类型的时候,你可能会对代码中的变量、对象或函数的状态没有信心。状态包括您正在检查的事物的当前内容及其当前类型。PHP 提供了几个方便的函数,print_r
和var_dump
,帮助您“查看”代码中发生了什么。见清单 2-1 。
<?php
define('MY_CONSTANT', 'banana');
$my_function = function ($data) {
return $data;
};
$my_array = [1,2,'apple',MY_CONSTANT,$my_function];
echo "print_r output :\n\n";
print_r($my_array);
echo "\n\n var_dump output :\n\n";
var_dump($my_array);
Listing 2-1.examine.php
运行清单 2-1 中的脚本给出清单 2-2 中所示的输出。
print_r output :
Array
(
[0] => 1
[1] => 2
[2] => apple
[3] => banana
[4] => Closure Object
(
[parameter] => Array
(
[$data] => <required>
)
)
)
var_dump output :
array(5) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
string(5) "apple"
[3]=>
string(6) "banana"
[4]=>
object(Closure)#1 (1) {
["parameter"]=>
array(1) {
["$data"]=>
string(10) "<required>"
}
}
}
Listing 2-2.
examine-output.txt
如您所见,这些函数产生了相似的输出。print_r
的格式便于人们阅读,而var_dump
提供了关于基本类型的更多信息。我通常使用var_dump
,当我有一个特别密集的数据结构要查看时,返回到print_r
,因为格式化可以使它更容易。
函数式编程中另一个特别有用的函数是debug_print_backtrace()
。函数式编程通常包括将许多单一用途的函数组合成代表程序的函数栈。当一个错误发生时,很难准确地找到堆栈中使用的函数中的哪一个导致了错误。回溯显示您当时在函数调用堆栈中的位置,通常由调试器和代码分析器显示。正如清单 2-3 中的人为示例所展示的那样,debug_print_backtrace()
函数可以允许您从代码中打印调用堆栈(清单 2-4 显示了输出)。
<?php
function prepare_text($text) {
return make_headline($text);
}
function make_headline($text) {
return add_h_tags( upper_case($text) );
}
function upper_case($text) {
return strtoupper($text);
}
function add_h_tags($text) {
debug_print_backtrace();
return '<h1>'.$text.'</h1>';
}
$title = prepare_text('testing');
echo $title;
Listing 2-3.
backtrace.php
#0 add_h_tags(TESTING) called at [backtrace.php:12]
#1 make_headline(testing) called at [backtrace.php:6]
#2 prepare_text(testing) called at [backtrace.php:30]
<h1>TESTING</h1>
Listing 2-4.backtrace-output.txt
列表的顶部是最近调用的函数(在本例中为add_h_tags()
),一直到初始函数(prepare_text()
)。注意,虽然make_headline(
函数调用了upper_case()
函数,但它不在回溯中。这是因为它已经完成了它的执行,并且在返回它自己的输出之前没有等待链中的下一个函数的输出(其他三个函数也是这种情况,它们仍然在堆栈中)。
当你学习和试验代码时,提到的三个函数是最有用的,特别是如果你使用一个读-求值-打印循环(read 更多信息请参见附录 B)来测试和破解代码。在正确的开发和生产代码中,您应该使用调试器、分析器和安全日志记录来跟踪您的代码在做什么;使用print_r
和var_dump
可能会意外地将内部数据泄露给外界,并导致各种安全问题。
可变性和不变性
如果某样东西是可变的,那就意味着你可以改变它。变量是可变的。考虑清单 2-5 和清单 2-6 中的代码。
<?php
$a = 1;
var_dump($a);
$a = 2;
var_dump($a);
$a = "Banana";
var_dump($a);
Listing 2-5.mutable.php
int(1)
int(2)
string(6) "Banana"
Listing 2-6.mutable-output.txt
首先,$a
设置为 1。然后,因为它是可变的,所以您可以将其“变异”(更改)为等于 2。最后,你再次将其突变为Banana
。注意,在最后一次更改中,您不仅改变了变量,还改变了类型,从int
变成了string
。
在函数式编程中,您希望值(由函数表示)是不可变的。这有助于你对程序进行推理,并允许你将函数松散地耦合在一起。稍后您将更详细地了解这一点。
PHP 对不变性的支持有限,主要是以使用define()
函数或const
关键字定义的“常量”的形式。使用define()
和const
声明常量的方式和内容有一些不同,但是一旦声明,这两种方法创建的常量是相同的。它们的一个共同点是只有标量或数组可以是常量。清单 2-7 试图从包含匿名函数的变量中创建一个常量。清单 2-8 显示了输出。
<?php
$double = function ($input) {
return $input * 2;
};
define('DOUBLE',$double);
echo "Double 2 is " . $double(2) . "\n";
echo "Double 2 is " . DOUBLE(2) . "\n";
Listing 2-7.
constant-func.php
PHP Warning: Constants may only evaluate to scalar values or arrays in constant-func.php on line 8
Double 2 is 4
PHP Fatal error: Uncaught Error: Call to undefined function DOUBLE() in constant-func.php:12
Stack trace:
#0 {main}
thrown in constant-func.php on line 12
Listing 2-8.constant-func-output.txt
在这里你可以看到,当你试图使用保存在define()
中的函数的变量时,你得到一个警告,当你试图使用DOUBLE
常量时,你得到一个确认(通过一个致命错误),它确实没有被定义。
因此,在没有 PHP 太多帮助的情况下,您需要在编码时通过自律来确保不变性。有助于实现这一点的一个关键方法是避免使用赋值,在阅读本书的过程中,你会看到实现这一点的方法。当你告诉人们你正在使用 PHP 进行函数式编程时,他们会指出的主要问题之一就是 PHP 缺乏对不变性的支持(与其他语言相比)。然而,它不会以任何方式阻止你用 PHP 编写函数式程序;你只需要在编码时记住这一点。
除了观察自己做了什么,您还需要关注 PHP 在做什么。要考虑的一个关键问题是 PHP 自己的函数如何操作你的变量。例如,函数sort()
对传递给它的数组进行变异(即排序),而不是返回一个新数组,该新数组是旧数组的排序版本(并保持旧数组不变异)。然而,您可以非常容易地制作自己的不可变版本的sort()
(参见清单 2-9 和清单 2-10 )。
<?php
function immutable_sort($array) {
sort($array);
return $array;
}
$vegetables = ['Carrot', 'Beetroot', 'Asparagus'];
# Sort using our immutable function
$ordered = immutable_sort( $vegetables );
print_r( $ordered );
# Check that $vegetables remains unmutated
print_r( $vegetables );
# Do it the mutable way
sort( $vegetables );
# And see that the original array is mutated
print_r( $vegetables );
Listing 2-9.
sort.php
Array
(
[0] => Asparagus
[1] => Beetroot
[2] => Carrot
)
Array
(
[0] => Carrot
[1] => Beetroot
[2] => Asparagus
)
Array
(
[0] => Asparagus
[1] => Beetroot
[2] => Carrot
)
Listing 2-10.sort-output.txt
这是可行的,因为默认情况下 PHP 函数参数是通过值传递的,而不是通过引用。这意味着当你调用一个函数时,它得到的是你作为参数给出的任何变量的副本,而不是对变量本身的引用。函数对该副本做的任何事情都不会影响原始变量。PHP 允许你通过引用传递一个参数(这是sort()
用来改变原始数组的),但这不是默认的。当您传入一个对象或资源时,您传入的是一个对象或资源变量,它是指向该对象或资源的指针。变量仍通过值传递;但是,变量的新副本仍然指向原始对象或资源,因此它的行为方式类似于按值传递。你会在第七章中深入探讨这个问题。
在大多数情况下,很明显哪些函数会使其参数发生突变;它们通常不将输出作为返回值,但有些混合了按值和按引用参数,所以如果不确定,请查看 PHP 手册。
进一步阅读
- PHP 手册中的常量
define()
和const
之间差异的综合概述
什么是函数?
您可能对什么是函数有一个合理的概念,并且您可能经常在您的 PHP 代码中使用它们。然而,我将从头开始介绍函数,因为很好地理解 PHP 如何实现函数的基础知识,以及处理它们的不同方式,对于理解如何用 PHP 实现函数式编程是必要的。
通过本章的学习,你会对函数编程中函数的确切含义有更好的理解。但是这里有一个很好的开始定义:
函数是一组指令,封装在一个独立的、可重用的代码块中。
PHP 允许您使用几种不同的函数调用,接下来您将依次查看这些函数。
命名函数
标准命名函数是 PHP 中使用函数的基本方式。一个命名函数看起来有点像清单 2-11 中的my_function()
函数(输出如清单 2-12 所示)。
<?php
function my_function ( $parameter_1, $parameter_2) {
$sum = $parameter_1 + $parameter_2;
return $sum;
}
$value1 = my_function(10,20);
var_dump( $value1 );
$value2 = my_function(6,9)
var_dump( $value2 );
Listing 2-11.
named_function.php
int(30)
int(15)
Listing 2-12.named_function-output.txt
该函数是使用“函数”语言构造创建的。它有一个名字(my_function
),后来用来称呼它。它有参数(在本例中是两个),允许您将值传递给函数内部的代码。它执行一些有用的工作(在这种情况下,将参数相加并返回它们)。它有一个设置函数值的return
语句。从这个例子中可以看出,函数的值通常依赖于外部值,在这个例子中是通过改变作为输入给出的参数。然而,返回值可能依赖于参数没有直接传入的外部状态源,如清单 2-13 和清单 2-14 所示。
<?php
$oranges = 3;
function count_fruit($apples, $bananas) {
global $oranges;
$num_fruit = $apples + $bananas + $oranges;
return $num_fruit;
}
function get_date() {
return trim ( shell_exec('date') );
}
var_dump( count_fruit(6,7) );
var_dump( get_date() );
Listing 2-13.
returns.php
int(16)
string(28) "Tue 21 Feb 13:12:37 GMT 2017"
Listing 2-14.returns-output.txt
count_fruit()
在返回值的计算中使用全局变量$orange
,其值在函数外部设置。get_date()
根本不需要任何参数,根据外部 shell 命令计算返回值。在这两种情况下,这些都是“副作用”的潜在原因,您将在后面看到,并表明 PHP 中的函数并不仅限于对所提供的参数进行操作。
这是数学函数的一个关键区别。函数式编程中的函数是指函数的数学概念,而不是函数的编程概念。数学函数是纯函数,我们很快就会看到。
以下是命名函数的主要限制:
- 他们不能被摧毁。
- 它们的功能(函数中的代码)一旦定义就不能更改。
- 它们更难“传递”,因为它们不能被赋给一个变量。
- 只能将函数名赋给变量,而不是函数本身。
“动态”处理命名函数的选项有限,尽管call_user_func()
函数确实提供了一种以这种方式工作的方法,如清单 2-15 和清单 2-16 所示。
<?php
function list_fruit($item) {
return ['apple','orange','mango'][$item];
}
function list_meat($item) {
return ['pork','beef','human'][$item];
}
$the_list = 'list_fruit';
var_dump( call_user_func($the_list, 2) );
$the_list = 'list_meat';
var_dump( call_user_func($the_list, 1) );
Listing 2-15.
userfunc.php
string(5) "mango"
string(4) "beef"
Listing 2-16.userfunc-output.txt
正如您所看到的,您可以向call_user_func()
传递一个函数的名称(作为一个字符串)(加上您想要提供给该函数的任何参数),并且call_user_func()
将返回您调用的函数的返回值作为它自己的返回值。如您所见,您可以在$the_list
中更改函数的名称(因为它是一个字符串变量)并再次运行call_user_func()
,这次运行一个不同的函数。这给了你一点动力,但是非常有限。类似的方法称为变量函数,您将在下一节中看到。从 PHP 7.0 开始,您还可以使用 PHP 的 closure 对象的fromCallable
静态方法将一个命名函数包装成一个闭包,稍后您将看到这个闭包。
命名函数的范围也是不直观的。正如你将在本章后面的“作用域”一节中看到的,当你在一个函数中创建一个变量时,默认情况下,它对该函数之外的代码是不可用的。然而,当一个命名函数在另一个函数中被实例化时,它是在全局范围内创建的,因此它可以从任何地方被调用,因此它也需要有一个全局唯一的名称。考虑清单 2-17 中嵌套函数的演示,它返回一个字符串来说明它们的嵌套(清单 2-18 显示了输出)。
<?php
function a() {
function b() {
return "a -> b";
}
return "a";
}
function c() {
function d() {
function e() {
return "c -> d -> e";
}
return "c -> d";
}
return "c";
}
var_dump( a() );
var_dump( b() );
var_dump( c() );
var_dump( d() );
var_dump( e() );
Listing 2-17.
name-scope.php
string(1) "a"
string(6) "a -> b"
string(1) "c"
string(6) "c -> d"
string(11) "c -> d -> e"
Listing 2-18.name-scope-output.txt
请注意,您是在其他函数的范围内定义函数b()
、d()
和e()
,但是当您用var_dump
调用它们时,您是在它们的“父”函数之外调用它们。您可以修改这个脚本来显示命名函数的另一个属性;在定义它们的作用域创建之前,不会创建它们。在清单 2-19 中,您交换了在var_dump()
部分调用c()
和d()
的顺序(输出如清单 2-20 所示)。
<?php
function a() {
function b() {
return "a -> b";
}
return "a";
}
function c() {
function d() {
function e() {
return "c -> d -> e";
}
return "c -> d";
}
return "c";
}
var_dump( a() );
var_dump( b() );
var_dump( d() );
var_dump( c() );
var_dump( e() );
Listing 2-19.
name-scope2.php
string(1) "a"
string(6) "a -> b"
PHP Fatal error: Uncaught Error: Call to undefined function d() in name-scope2.php:38
Stack trace:
#0 {main}
thrown in name-scope2.php on line 38
Listing 2-20.name-scope2-output.txt
因为你还没有调用c()
,d()
不存在,所以你会得到一个致命的错误。可以很好地访问b()
,因为您已经调用了a()
,即使您在调用它时是在主程序范围内。作为命名函数问题的最后一个演示,让我们看看对全局唯一名称的需求。对于普通变量,您可以对不同的变量使用相同的变量名,只要它们在不同的范围内(例如,在不同的函数中)。对于一个已命名的函数,这是行不通的,正如你从清单 2-21 和清单 2-22 中看到的。
<?php
function f() {
function g() {
return "1st g()";
};
return "f()";
}
function h() {
function g() {
return "2nd g()";
};
return "h()";
}
var_dump( f() );
var_dump( g() );
var_dump( h() );
Listing 2-21.
name-scope3.php
string(3) "f()"
string(7) "1st g()"
PHP Fatal error: Cannot redeclare g() (previously declared in name-scope3.php:7) in name-scope3.php on line 17
Listing 2-22.name-scope3-output.txt
如你所见,你已经定义了两次g()
,一次在f()
中,一次在h()
中。尽管如此,当您调用f()
和g()
时,事情一开始运行得很顺利,但是当您试图调用h()
时,g()
的第二个实例试图声明自己,导致了致命的错误。
为函数使用惟一的名字似乎并不是一个可怕的限制,除非你考虑到如果你开始用include()
或require()
或通过自动加载器来包含外部代码库,这些将成为你的函数名必须惟一的范围的一部分,并且很难保证其他人不会侵犯你的函数名!PHP 允许您“命名”函数,这在一定程度上缓解了这个问题;然而,这可能被认为是一个有点不优雅的解决方案(取决于你和谁交谈)。
可变函数
PHP 支持“可变”函数的概念。这是一种调用命名函数的动态方式,语法比您在上一节中看到的call_user_func()
示例稍微简洁一些。本质上,如果您在变量的末尾放入圆括号(圆括号),PHP 将调用存储在该变量的值中的命名函数(使用您放在圆括号中的任何参数)。参见清单 2-23 和清单 2-24 。
<?php
function vehicles( $index ) {
$types = ["car", "motorbike", "tractor"];
return $types[$index];
}
function animals( $index ) {
$types = ["cow", "pig", "chicken", "horse"];
return $types[$index];
}
$get_thing = 'animals'; # string with the name of a function
var_dump( $get_thing(2) ); # add ($index) to call it
$get_thing = 'vehicles'; # change the function
var_dump( $get_thing(2) ); #same "code", different function
# Just to show that $get_thing is just a
# standard string, and nothing special...
$get_thing = strrev('selcihev'); # do string things
var_dump( $get_thing ); # it's a string
var_dump( $get_thing(2) ); # call it
var_dump( $get_thing ); # afterwards, still just a string
unset( $get_thing ); # we can destroy it, because it's a string
var_dump( $get_thing );
var_dump( vehicles(2) ); # But the function still exists
# However, it needs to be set to a function that exists
$get_thing = 'people';
var_dump( $get_thing(2) );
Listing 2-23.variable.php
string(7) "chicken"
string(7) "tractor"
string(8) "vehicles"
string(7) "tractor"
string(8) "vehicles"
PHP Notice: Undefined variable: get_thing in variable.php on line 41
NULL
string(7) "tractor"
PHP Fatal error: Uncaught Error: Call to undefined function people() in variable.php:49
Stack trace:
#0 {main}
thrown in variable.php on line 49
Listing 2-24.variable-output.txt
如您所见,变量$get_thing
只是一个字符串,保存您想要调用的函数的名称,您可以随时更改该名称。然而,实际的函数就像命名的函数一样运行(因为它们就是这样)。
语言结构
那是一个函数,对吗?没错。echo()
怎么样?它有像strtoupper()
一样的圆括号,并且带参数,所以它一定是一个函数,对吗?抱歉,没有!一些 PHP“函数”实际上根本不是函数,而是“语言结构”,是语言语法的内置部分。您通常可以发现这些,因为即使它们接受圆括号中的参数,您也不必使用圆括号。你也可以阅读 PHP 手册中的相关页面来了解它们是什么。类似功能的结构的例子包括echo()
、print()
、unset()
、isset()
、empty()
、include()
和require()
。语言结构和功能之间的区别有时很重要。前一节中描述的变量函数不能用于语言结构。参见清单 2-25 和清单 2-26 。
<?php
$var_func = 'echo';
$var_func('hello world!');
Listing 2-25.constructs.php
PHP Fatal error: Uncaught Error: Call to undefined function echo() in constructs.php:5
Stack trace:
#0 {main}
thrown in constructs.php on line 5
Listing 2-26.constructs-output.php
然而,如果你确实需要像对待函数一样对待一个构造,那么你需要做的就是把它包装在你自己的函数中,如清单 2-27 和清单 2-28 所示。
<?php
function my_echo($string) {
echo $string;
}
$var_func = 'my_echo';
$var_func('hello world!');
Listing 2-27.constructs2.php
hello world!
Listing 2-28.constructs2-output.php
返回值
正如您在“命名函数”一节中看到的,您可以使用一个return
语句为您的函数赋值(或返回值)。为了更好地处理函数,您需要理解返回值的几个属性。
如果你没有在你的函数中放一个return
语句,或者如果一个特定运行的执行路径没有命中一个return
语句,那么你的函数将返回NULL
。参见清单 2-29 和清单 2-30 。
<?php
function reverse($string) {
$string = strrev($string);
}
function capitals($string) {
if ($string != 'banana') {
$string = strtoupper($string);
return $string;
}
}
# no return statement
var_dump( reverse('hello') );
# returns a value
var_dump( capitals('peaches') );
# execution flow misses return statement
var_dump( capitals('banana') );
Listing 2-29.null-return.php
NULL
string(7) "PEACHES"
NULL
Listing 2-30.null-return-output.txt
在reverse()
函数中,您完全忘记了返回任何值,所以反转的字符串没有返回到函数之外。Captials()
大写的桃子罚款,但香蕉没有通过一个代码路径与return
声明,所以你只是得到了一个NULL
回你的麻烦。
同理,不带参数的return
语句也返回NULL
,如清单 2-31 和清单 2-32 所示。
<?php
function fruits($type) {
if ($type == 'mango') {
return 'Yummy!';
} else {
return;
}
}
var_dump( fruits('kiwi') );
var_dump( fruits('pomegranate') );
var_dump( fruits('mango') );
Listing 2-31.null-return2.php
NULL
NULL
string(6) "Yummy!"
Listing 2-32.null-return2-output.txt
在 kiwi 和石榴上调用fruits()
命中了第二个没有参数的return
语句,所以为它们返回了NULL
。芒果,可能是有史以来最大的水果,导致fruits()
函数代码路径命中第一个return
语句,该语句将Yummy!
字符串作为参数,因此在这种情况下fruit()
返回该字符串。
关于return
语句需要注意的另一点是,它可以出现在函数中的任何一点,并在该点立即终止函数的执行。参见清单 2-33 和清单 2-34 。
<?php
function my_funct() {
$a = 23;
return $a;
$a = 45;
return $a;
}
var_dump( my_funct() );
Listing 2-33.return.php
int(23)
Listing 2-34.return-output.txt
正如你所看到的,函数在第一个return
语句后返回,设置$a
等于 45 的代码(以及随后的第二个return
调用)永远不会被执行。
总而言之,在调用return
之前,你需要确保你已经完成了所有需要做的处理,并确保在函数结束之前,你的所有代码路径都命中了一个return
语句。
λ/匿名函数
您已经看到了“传统的”命名函数,并看到了它们的一些缺点。幸运的是,从 PHP 5.4 开始,您可以在 PHP 中使用匿名函数。在其他语言中,这些函数也被称为匿名函数文字或 lambda 函数。它们被称为匿名函数,因为与命名函数不同,它们没有函数名。你的第一个问题可能是,“那么,你怎么称呼他们?”有许多不同的方法可以做到这一点,但是首先看一下 PHP 是如何“在幕后”实现匿名函数是有帮助的参见清单 2-35 和清单 2-36 。
<?php
var_dump(
# An anonymous function
function ($a) { return $a; }
);
Listing 2-35.anon.php
object(Closure)#1 (1) {
["parameter"]=>
array(1) {
["$a"]=>
string(10) "<required>"
}
}
Listing 2-36.anon-output.txt
您可以看到,函数定义与命名函数的定义相同,只是没有名称。查看var_dump
的输出,您可以看到该函数实际上是一个对象(属于Closure
类,您将在本章后面看到)。
那是什么意思?这意味着你可以像对待 PHP 中的其他对象一样对待它。你可以把它赋给一个变量,你可以传递它,销毁它,复制它,等等。但是你如何调用它来得到有用的东西呢?最直接的方法是将它赋给一个变量,然后使用之前学过的方法(变量函数和call_user_func
)来执行它。PHP 可以从变量的类型(闭包对象而不是字符串)判断出它是匿名函数而不是命名函数,并知道如何处理它。让我们看一些例子。见清单 2-37 和清单 2-38 。
<?php
# create an anonymous function and assign it to a variable
$double = function ($a) { return $a * 2; };
var_dump( $double );
# call the anonymous function
var_dump( $double(4) );
var_dump( call_user_func($double, 8) );
# Copy it to another variable;
$two_times = $double;
var_dump( $two_times(4) + $double(6) );
# pass it as a parameter to another function
$numbers = [1,2,3,4];
var_dump( array_map( $two_times, $numbers ) );
# redefine it
$double = function ($a) { return $a * 4; };
var_dump( $double(10) );
# but the earlier copy is definitely a copy not a reference
var_dump( $two_times(10) );
# destroy it
unset($double);
var_dump( $double(9) );
Listing 2-37.call_anon.php
object(Closure)#1 (1) {
["parameter"]=>
array(1) {
["$a"]=>
string(10) "<required>"
}
}
int(8)
int(16)
int(20)
array(4) {
[0]=>
int(2)
[1]=>
int(4)
[2]=>
int(6)
[3]=>
int(8)
}
int(40)
int(20)
PHP Notice: Undefined variable: double in call_anon.php on line 39
PHP Fatal error: Uncaught Error: Function name must be a string in call_anon.php:39
Stack trace:
#0 {main}
thrown in call_anon.php on line 39
Listing 2-38.call_anon-output.txt
你不需要总是将匿名函数赋给一个有用的变量。例如,您可以将它们定义为其他函数的参数。参见清单 2-39 和清单 2-40 。
<?php
# Define a function that is assigned to a variable,
# that takes a function as its first parameter
# and a parameter to call that function with as its
# second
$function_caller = function ($function, $parameter) {
$function($parameter);
};
# Define a named function
function double($a) {
echo ($a * 2)."\n";
}
# use the anonymous function to call the named function
# using the Variable Function technique
$function_caller('double', 4);
# this time, define a new anonymous function right in the
# calling parameter code
$function_caller( function($a) { echo 'Function says ' . $a . "\n"; },
'Hello There');
# Do it again, but with a different anonymous function. Note that
# the anonymous function no longer finishes once the function
# has finished executing.
$function_caller( function($a) { echo $a . ' backwards is '. strrev($a) . "\n"; },
'Banana');
# It's not only our own functions that can accept inline definitions
# of anonymous functions ...
var_dump(
array_map( function($a) { return $a * 2; }, [1,2,3,4])
);
Listing 2-39.call_anon2.php
8
Function says Hello There
Banana backwards is ananaB
array(4) {
[0]=>
int(2)
[1]=>
int(4)
[2]=>
int(6)
[3]=>
int(8)
}
Listing 2-40.call_anon2-output.txt
高阶函数
高阶函数是一个可以接受一个或多个函数作为输入和/或返回一个或多个函数作为输出的函数,我在上一节的结尾已经提到了它们。高阶函数是作用于其他函数的函数。在函数式编程中,您将看到使用高阶函数进行程序控制(而不是像for
和while
这样的命令式控制语句)。
在本章的前几节中,我已经介绍了高阶函数所需要的构件,但是如果您还没有完全理解,那么可以认为您已经发现了匿名函数实际上是 PHP 内部的对象。您可能熟悉在 PHP 中传递传统对象,您也可以对函数对象做同样的事情。它们可以用作函数的参数,并作为函数的结果返回。我们来看一个例子;见清单 2-41 和清单 2-42 。
<?php
# Define some data. I should stop writing code
# when I'm hungry...
$fruits = ['apple', 'banana', 'blueberry', 'cherry'];
$meats = ['antelope','bacon','beef','chicken'];
$cheeses = ['ambert','brie','cheddar','daralagjazsky'];
# Create a function that filters an array and picks out the
# elements beginning with a specified letter
$letter_filter = function($list, $letter) {
# Rather than a foreach loop or similar, we'll use PHP's
# higher-order array_filter function. Note it takes two
# paramters, an array ($list in our case) and a
# function (which we've defined inline)
return array_filter($list, function($item) use ($letter) {
return $item[0] == $letter;
});
};
# We can call the function on our data as normal.
print_r( $letter_filter($fruits,'a') );
print_r( $letter_filter($meats,'b') );
print_r( $letter_filter($cheeses,'c') );
# But let's use a single call to the higher-level array_map function
# to demonstrate a simple "loop" over three arrays & parameters.
# It should give the same output as the three functions above,
# wrapped up into a single array.
print_r(
array_map( $letter_filter, [$fruits, $meats, $cheeses], ['a', 'b', 'c'])
);
Listing 2-41.higher-order.php
Array
(
[0] => apple
)
Array
(
[1] => bacon
[2] => beef
)
Array
(
[2] => cheddar
)
Array
(
[0] => Array
(
[0] => apple
)
[1] => Array
(
[1] => bacon
[2] => beef
)
[2] => Array
(
[2] => cheddar
)
)
Listing 2-42.higher-order-output.txt
最后一个array_map
子句可能会给你一些提示,告诉你如何使用函数替换循环和其他命令式程序控制结构。
您可能还发现,在您用作array_filter
最后一个参数的匿名函数中,有一个use ($letter)
子句。array_filter
没有提供任何方法让你传递额外的变量给过滤函数,而且$letter
在执行的时候不在函数的范围之内。use
允许你将数据绑定到你的函数上,这样你就可以利用通常情况下它无法利用的变量。我将在下一节讨论作用域,并在讨论闭包时更多地讨论use
子句。
不过,这并不完全是将函数作为输入来使用;高阶函数通常也会将它们作为输出返回。让我们来看一个例子。参见清单 2-43 和清单 2-44 。
<?php
# Create a higher order function to return a
# function to "add" two variables using a user selectable
# method
function add($method) {
if ($method == 'sum') {
# our return value is actually an anonymous function
return function($a, $b) { return $a+$b;};
} else {
# this is returning a different function object
return function($a, $b) { return $a.$b; };
}
}
# Let's call the function. Note, that as the function
# returns a function, we can simply stick an extra
# set of parentheses on the end with some parameters
# to call that newly returned function.
print_r( add('sum')(2,3) ."\n" );
print_r( add('concatenate')('hello ', 'world!') ."\n" );
# We can also pass the function to returned to other
# higher order functions like array_map
$a = [1, 2, 'cat', 3, 'orange', 5.4];
$b = [6, 3, 'ch', 9.5, 'ish', 6.5];
print_r( array_map(add('sum'), $a, $b) );
print_r( array_map(add('concatenate'), $a, $b) );
# and we can assign the returned function to a
# variable as with any anonymous function
$conc = add('concatenate');
print_r( array_map($conc, $a, $b) );
print_r( $conc("That's all, ", "folks!\n") );
Listing 2-43.higher-order2.php
5
hello world!
Array
(
[0] => 7
[1] => 5
[2] => 0
[3] => 12.5
[4] => 0
[5] => 11.9
)
Array
(
[0] => 16
[1] => 23
[2] => catch
[3] => 39.5
[4] => "orange" and "ish"
[5] => 5.46.5
)
Array
(
[0] => 16
[1] => 23
[2] => catch
[3] => 39.5
[4] => "orange" and "ish"
[5] => 5.46.5
)
That's all, folks!
Listing 2-44.higher-order2-output.txt
正如您在上一节中看到的,像echo()
这样的语言构造并不是真正的函数,所以您不能将它们直接用作高阶函数的输入或输出,但是如果您需要以这种方式使用它们,您总是可以将它们包装在您自己的自定义函数中。
范围
变量的范围决定了代码的哪些部分可以访问它。在 PHP 中,变量存在于全局范围内,除非在用户定义的函数中声明(包括作为函数的参数之一)。如果变量在全局范围内,则可以从程序中的任何地方访问它,除了在用户定义的函数内(默认情况下)。如果您想在函数中操作它,您需要要么通过引用将它作为参数传递,要么使用global
关键字将它拉入函数的范围。如果你把它作为一个参数通过值传递,你的函数会得到一个变量值的副本,但是即使你给它相同的名字,你的函数的副本也是独立于全局变量的。给函数的副本分配一个新值不会改变全局值。同样,如果你简单地在一个函数中声明一个与全局作用域中的变量同名的变量(或者实际上是在另一个函数中的一个变量),它们就是独立的变量,不会相互作用。
在编写函数式程序时,变量范围是一个需要牢记的重要概念。使用 PHP 进行函数式编程的一个好处是,您必须特意将全局范围内的变量引入到函数中,或者从外部更改函数中的值。在函数式编程中,你希望你的函数没有副作用,这意味着你不希望你的程序的外部状态(例如,全局变量)影响我们函数的操作,因为这使得很难准确地推断函数在任何给定的时间将做什么。请注意,当我谈论函数时,“外部状态”不仅仅是程序外部的任何东西(数据库、系统状态等)。)但是函数之外的任何状态(例如,程序中的其他状态)没有通过参数传递给函数。
对作用域规则的一点小小的警告是所谓的超全局变量,这些变量实际上存在于程序的任何地方。$_SESSION
、$_GET
、$_POST
等。,是您可能从典型的基于 web 的 PHP 应用中了解到的例子。这里最好的建议就是避免从你的函数中访问它们!
清单 2-45 展示了作用域的一般规则(输出如清单 2-46 所示)。
<?php
# Define some variables in the "Global" scope
$a = 2;
$b = 6;
function double($a) {
# In this function's scope, we'll double $a
$a = $a * 2;
return $a;
}
# This is like the function above, except
# we've passed in the variable by reference
# (note the & before $a in the parameters)
function double_ref(&$a) {
$a = $a * 2;
return $a;
}
function double_add($a) {
# We'll pull in $b from the global scope
global $b;
# and double it
$b = $b * 2;
# and double $a from the local function scope
$a = $a * 2;
return $a + $b;
}
# a in the global scope = 2
echo("a = $a \n");
# a in the function scope, doubled, = 4
echo("double a = ". double($a). " \n");
# but a in the global scope still = 2
echo("a = $a \n");
# now we pass it in by reference
echo("double_ref a = ". double_ref($a). " \n");
# and now $a = 4;
echo("a = $a \n");
# b in the global scope = 6
echo("b = $b \n");
# doubled and added = 8 + 12 = 20
echo("double_add a+b = ". double_add($a). " \n");
# a is still = 4 in the global scope
echo("a = $a \n");
# but b in the global scope is now doubled = 12
echo("b = $b \n");
Listing 2-45.scope.php
a = 2
double a = 4
a = 2
double_ref a = 4
a = 4
b = 6
double_add a+b = 20
a = 4
b = 12
Listing 2-46.scope-output.txt
进一步阅读
状态
你是否曾经花了很长时间试图重现一个用户正在经历的错误,但运气不好?这可能是因为您无法重新创建用户的状态。状态是程序在任何给定时间运行的环境条件。这包括内部和外部条件。外部条件包括以下内容:
- 文件的内容
- 存储内容
- 网络条件
- 环境变量
- 其他正在运行的进程的状态
内部条件的例子包括:
- 变量和其他数据结构的值(和存在)
- 资源量(内存、磁盘空间等。)由进程使用
在函数式编程中,你尽量避免依赖状态。状态之所以不好,是因为它很难重现(就像之前重现用户问题的例子一样),也因为它很难对一段代码进行推理。例如,假设我给你一个用 PHP 编写的函数和一个已知的输入变量。你能告诉我那个函数的返回值是多少吗?如果函数需要的唯一信息是输入变量,那么是的,你可以。但是,如果函数引用了外部文件的内容,引用了全局变量的值,或者调用了远程 API,那么在不知道这些外部资源的确切状态的情况下,您将很难确定函数将实际返回什么。
当然,如果你不能从文件中读取或者与远程系统对话,你的程序将不会非常有用,所以你需要管理状态。正如您将在本书中看到的,您通常通过将任何必要的状态信息作为参数直接传递到您的函数链中来实现这一点,这样,给定函数的输入,您就可以完全确定它的输出。
参数/自变量/操作数、实参数和变元函数
参数、自变量和操作数。它们是同一事物的不同词汇,至少对我们的目的来说是这样。它们是用来显式地将值传递给函数的东西。您会看到这些术语在各种文献中互换使用(特别是当您开始探索函数编程的数学背景时)。在 PHP 手册中,术语参数通常(但不总是)被使用。为免存疑,在清单 2-47 中,我所说的事物为$arg1
、$stringB
等等。
<?php
function func_one($arg1, $arg2, $arg3) {
return $arg1 + $arg2 + $arg3;
};
function func_two($stringA, $stringB) {
return $stringA.$stringB;
};
Listing 2-47.arguments.php
Arity 是从数学和逻辑中借用的术语,在编程中用于描述函数接受的参数数量。清单 2-47 中的func_one
函数接受三个参数,因此有一个三进制数(有时称为三进制函数),而func_two
有一个二进制数,因为它接受两个参数。
现在,如果你有很多参数要传递给一个函数,函数定义将很快变得难以处理。如果您不知道您的调用代码需要使用多少个参数,会发生什么呢?这两种情况都是您在函数式编程中试图避免的,但是 PHP 一直是实用主义者,当您发现该规则的例外时,它会帮助您。它被称为变量函数,它是使用“splat”运算符实现的……(这是一行中的三个句号,称为 splat 运算符,而不是我在那句话的结尾拖尾)。注意使用三个句号,而不是 Unicode 省略号,看起来很像。splat 操作符允许您说“将所有剩余的参数放入这个数组”,如清单 2-48 和清单 2-49 所示。
<?php
function my_func($a, ...$b) {
var_dump($a);
var_dump($b);
}
# Call it with 4 arguments, $a will be a string
# and $b will be an array of 3 strings
my_func('apples', 'bacon', 'carrots', 'doughnut');
# Define some colorful arrays
$array1 = ['red', 'yellow', 'pink', 'green'];
$array2 = ['purple', 'orange', 'blue'];
# $a will be an array with 4 elements,
# $b will be an array with 1 element, which itself is an
# array of 3 elements
my_func($array1, $array2);
# We can also use the splat operator in reverse when
# calling an array. In this case, the splat
# unpacks $array2 into 3 separate arguments, so
# $b will be an array with 3 elements.
my_func($array1, ...$array2);
Listing 2-48.splat.php
string(6) "apples"
array(3) {
[0]=>
string(5) "bacon"
[1]=>
string(7) "carrots"
[2]=>
string(8) "doughnut"
}
array(4) {
[0]=>
string(3) "red"
[1]=>
string(6) "yellow"
[2]=>
string(4) "pink"
[3]=>
string(5) "green"
}
array(1) {
[0]=>
array(3) {
[0]=>
string(6) "purple"
[1]=>
string(6) "orange"
[2]=>
string(4) "blue"
}
}
array(4) {
[0]=>
string(3) "red"
[1]=>
string(6) "yellow"
[2]=>
string(4) "pink"
[3]=>
string(5) "green"
}
array(3) {
[0]=>
string(6) "purple"
[1]=>
string(6) "orange"
[2]=>
string(4) "blue"
}
Listing 2-49.splat-output.txt
从清单 2-48 和清单 2-49 中可以看到,当调用一个函数时,可以反向使用 splat 将一个数组分割成单独的参数。如果你正在使用类型提示(见第四章,你可以给变量参数添加一个类型提示,以确保所有传递的值都是正确的类型(假设你希望它们都是相同的类型)。
进一步阅读
关闭
在前一节中我简要地提到了闭包,但是现在我将更详细地解释它们。闭包是一个功能,连同一个被它“封闭”的环境。这有点像流线型的对象:函数是(唯一的)方法,环境由对象的属性组成。在 PHP 中,包含在闭包中的环境是一个或多个变量的集合。
正如您所看到的,匿名函数的一个重要特性是,您可以像传递变量或对象一样传递它们(正如我提到的,它们确实是作为 closure 类的对象实现的)。这样做的主要缺点是,当它们被传递时,它们通常会从一个范围传递到另一个范围(通常是在一个函数链中上下传递)。这意味着,当您在一个作用域中创建函数时,您不能确定它是否仍在该作用域中,以便在被调用时可以从该作用域中作为参数访问变量。
通过将匿名函数转换成闭包,您可以从当前作用域中的一个或多个变量上“关闭函数”。您可以在函数定义中使用一个use
子句来实现这一点。
假设您想要创建一个名为get_multiplier
的函数,该函数返回一个匿名函数来将任意数字乘以一个固定的量。第一次尝试这样的函数可能看起来像清单 2-50 。
<?php
function get_multiplier($count) {
return function ($number) {
return $number * $count;
};
}
$times_six = get_multiplier(6);
print_r( $times_six(3) );
Listing 2-50.closure.php
所以当你调用get_multiplier(6)
时,它返回(到$times_six
中)一个匿名函数,该函数将$number
乘以$count
(你指定为 6)。那么当你在 3 上调用匿名函数时(使用$times_six(3)
,你应该得到 18 的返回(3×6),对吗?清单 2-51 显示了实际发生的情况。
PHP Notice: Undefined variable: count in closure.php on line 7
0
Listing 2-51.closure-output.txt
嗯。它说$count
未定义。但是它是作为参数传递给get_multiplier
的,当你定义匿名函数的时候,你是在get_multiplier
里,不是吗?嗯,是也不是。一个函数,不管在哪里定义,都有它自己的作用域。在脚本的最后一行调用匿名函数的时候,无论如何,你肯定不在get_multiplier
的范围之内。因此,您需要使用一个use
子句将$count
包含在您的函数中(参见清单 2-52 和清单 2-53 )。
<?php
function get_multiplier($count) {
return function ($number) use ($count) {
return $number * $count;
};
}
$times_six = get_multiplier(6);
print_r( $times_six(3) );
Listing 2-52.closure2.php
18
Listing 2-53.closure2-output.txt
太好了。您可以使用use
子句在闭包中包含任何东西,包括变量、对象,当然(因为它们是作为对象实现的)其他闭包和匿名函数!只需在use
子句中添加一个逗号分隔的参数列表,就像在 function 子句中添加额外的参数一样。
副作用
在函数式编程中,你不希望你的函数产生或经历“副作用”当一个函数改变了它作用域之外的东西的状态,或者作用域之外的东西影响了函数的内部操作时,就会产生副作用。您唯一希望影响函数操作的是它的给定参数,您唯一希望函数具有的效果是设置一个适当的返回值。
以下是副作用的例子:
- 该函数改变存在于其范围之外的变量(例如,全局变量)
- 使用存在于其范围之外的变量值的函数(如
$_SESSION
) - 通过引用而不是值传递的参数
- 读入用户输入(例如,通过
readline()
) - 从文件中获取数据(例如,通过
file_get_contents()
) - 写入文件(例如,通过
file_put_contents()
) - 抛出异常(除了在函数本身中被捕获和处理的地方)
- 打印到屏幕或将数据返回给网络用户(例如,通过
echo()
) - 访问数据库、远程 API 和任何其他外部资源
你明白了。基本上,任何意味着函数不是完全独立的东西。
对透明性有关的
函数式编程中的引用透明性意味着,给定一个函数,只要它的参数输入是相同的,您总是可以用它的返回值替换那个函数。清单 2-54 显示了一个例子(输出如清单 2-55 所示)。
<?php
# This function is Referentially Transparent. It has no
# side effects, and its output is fully determined
# by its parameters.
function is_RT($arg1, $arg2) {
return $arg1 * $arg2;
}
# This function is not RT, it uses the rand() function
# which introduces a value (a side effect) that
# isn't passed in through the parameters
function is_not_RT($arg1, $arg2) {
return $arg1 * $arg2 * rand(1,1000);
}
# So let's call our RT function with the values 3 and 6
$val = is_RT(3,6);
# if it really is RT, then in an expression like the
# following, we can replace the function call...
$a = is_RT(3,6) + 10; # with itself
$b = $val + 10; # with the value it returned earlier
$c = 18 + 10; # with the hard coded value
# and all output should be the same
var_dump( $a == $b ); # true
var_dump( $a == $c ); # true
# The following demonstrates that this is not the case
# for non-RT functions
$val = is_not_RT(3,6);
$a = is_not_RT(3,6) + 10;
$b = $val + 10;
$c = 2372 + 10;
#(2372 was the value from my first run of this script)
var_dump( $a == $b ); # false
var_dump( $a == $c ); # false
Listing 2-54.referential.txt
bool(true)
bool(true)
bool(false)
bool(false)
Listing 2-55.referential-output.txt
只有在没有副作用的情况下,引用透明才是可靠的。
纯函数
用函数式编程的术语来说,一个纯函数是没有副作用的,并且是引用透明的。纯函数是你在函数式编程中要写的东西,
列表和收藏
当阅读关于函数式编程的主题时,您会遇到对数据结构的引用,如列表和集合。在不同的语言中,这种数据结构有不同的优缺点,但是在 PHP 中,你有一把数据结构类型的瑞士军刀(容易让人误解)叫做数组。
PHP 中的数组实际上是作为“有序映射”在后台实现的,但它的实现足够灵活,可以像普通数组一样被视为列表、哈希表、字典、集合、堆栈、队列和其他不太常见的数据结构。所以,这就是你在这本书里会用到的,如果你试图用 PHP 实现一个算法,你在另一种语言里发现了一个列表,比方说,你通常可以对自己说"那将是一个数组。"列表和集合可能是函数式编程中最常见的数据结构(除了整数和字符串之类的简单类型),您只需将它们实现为标准数组。标准 PHP 库(SPL)确实提供了一些更有效的数据结构,但是它们可能更难处理,并且不是函数式编程所必需的,所以我不会在本书中涉及它们。如果你对它们感兴趣的话,可以看看 PHP 手册了解更多信息。
进一步阅读
- 标准 PHP 库(SPL)
结论
唷,这涉及了很多内容,但是您现在应该已经熟悉了开始掌握函数式编程所需的关键概念和术语。在接下来的几章中,当你开始将这些概念拼凑成一些函数式程序时,你将会看到一些诱人的特性。如果您仍然不知道什么是函数式编程,请不要担心;你才刚刚开始!
三、函数式模式入门
在前一章中,您已经了解了一些在开发函数式代码时会用到的关键概念和词汇。现在,您将把注意力转向一些核心编程模式,这些模式将功能性代码与命令性代码区分开来,并将用于构建功能性代码流。您将首先了解使用 map、filter 和 reduce 函数对数据集执行常见操作的一些方法。它们在大多数情况下都很有用,作为一名命令式程序员,您可能会使用基于foreach
或for
的循环来遍历一组数据。然后,您将看到递归函数,并探索它们对某些问题的好处。最后,您将看到部分功能和功能组合,它们是不同用途的混合功能的不同方式。
映射、过滤和减少
取代函数式程序中典型循环的一些关键函数式模式是 map、filter 和 reduce 函数,它们简化了对数据集合的迭代。在 PHP 中,已经为您实现了三个本机函数(array_map
、array_filter
和array_reduce
),可以用于您喜欢的数据类型。每个功能的目的如下:
Array_map
:将一个函数映射(应用)到一个数组的所有元素,返回一个新数组,每个映射的输出作为一个元素。输出数组将具有与输入数组相同数量的元素。Array_filter
:通过对每个元素应用一个函数来决定它是否出现在输出数组中,从而将一个数组的元素过滤成一个更小的数组。输出数组的大小等于或小于输入数组。Array_reduce
:通过对每个元素依次应用一个函数,将数组缩小为一个值。当对下一个元素调用该函数时,该函数每次调用的输出都作为参数反馈给函数。最后一次函数调用的输出值就是array_reduce
返回的值。
这些在实际操作中更容易理解,所以让我们来看一个使用所有这三者的示例脚本(参见清单 3-1 和清单 3-2 )。戴上你的厨师帽,想象你正在开一家新餐馆。你需要为你的客人做一道新菜寻找灵感。您的功能将采用一系列的原料和菜肴类型,提出一些令人兴奋的食谱,并最终挑选出最适合您的食谱来为您的客人烹饪。什么可能会出错…?
<?php
# Set up some arrays of data
$ingredients = [
"cod", "beef", "kiwi", "egg", "vinegar"
];
$dish_types = [
"pie", "smoothie", "tart", "ice cream", "crumble"
];
$baked = [
"pie", "tart", "crumble", "cake"
];
# A function which creates a "recipe" by combining
# an ingredient with a type of dish
$make_recipe = function ($ingredient, $dish) {
return $ingredient.' '.$dish;
};
# A function to check if a recipe involves baked
# goods by seeing if it has any of the words in
# the $baked array in it
$is_baked = function ($recipe) use ($baked) {
# We need to return a value that evaluates to
# true or false. We could use a foreach to loop
# through each $baked item and set a flag to true
# but instead we'll do it the functional way and
# filter the $baked array using a function that calls
# strpos on each element. At the end, if no match is
# made, array_filter returns an empty array which
# evaluates to false, otherwise it returns an array of
# the matches which evaluate to true
return array_filter($baked,
function($item) use ($recipe) {
return strpos($recipe, $item) !== false;
}
);
};
# A function which returns the longest of $current_longest or $recipe
$get_longest = function ($current_longest, $recipe) {
return strlen($recipe) > strlen($current_longest) ?
$recipe : $current_longest;
};
# the PHP function shuffle is not immutable, it changes the array it is
# given. So we create our own function $reshuffle which is immutable.
# Note that shuffle also has a side effect (it uses an external source
# of entropy to randomise the array), and so is not referentially
# transparent. But it will do for now.
$reshuffle = function ($array) { shuffle($array); return $array;};
# Now we actually do some work.
# We'll take a shuffled version of $ingredients and $dish_types (to add
# a little variety) and map the $make_recipe function over them, producing
# a new array $all_recipes with some delicious new dishes
$all_recipes = array_map($make_recipe,
$reshuffle($ingredients),
$reshuffle($dish_types)
);
print_r($all_recipes);
# Everyone knows that only baked foods are nice, so we'll filter
# $all_recipes using the $is_baked function. If $is_baked returns
# false for a recipe, it won't appear in the $baking_recipes output array.
$baking_recipes = array_filter($all_recipes, $is_baked);
print_r($baking_recipes);
# Finally we need to pick our favorite dish, and everyone knows that food
# with the longest name tastes the best. $get_longest compares two strings
# and returns the longest. Array_reduce applies the $get_longest
# function to each element of $baking_recipes in turn, supplying the result
# of the last call to $get_longest and the current array element. After all
# elements have been processed, the result of the last $get_longest call
# must be the longest of all of the elements. It is returned as the output
$best_recipe = array_reduce($baking_recipes, $get_longest, '');
print_r($best_recipe);
Listing 3-1.map_filter_reduce.php
Array
(
[0] => kiwi smoothie
[1] => vinegar crumble
[2] => egg ice cream
[3] => cod tart
[4] => beef pie
)
Array
(
[1] => vinegar crumble
[3] => cod tart
[4] => beef pie
)
vinegar crumble
Listing 3-2.map_filter_reduce-output.txt
所以,这就是你要的——醋碎。听起来很好吃(这可能是我是程序员而不是厨师的原因)。但是,举例来说,为什么要使用诸如 map、filter 和 reduce 之类的函数来代替foreach
循环呢?可重用性是主要优势。就其本身而言,诸如foreach
、while
、for
等循环在每次需要使用时都需要重写。当然,你可以将它们包装成自己定制的函数,当然,如果你需要内置的array_*
函数不能完全实现的东西,那么无论如何你都需要这样做,但是如果内置的函数确实符合你的用例,那么使用它们通常会 a)节省你创建自己的函数的时间,并且 b)通常比你自己的实现更高效。将像这样的通用控制结构包装到函数中(作为原生 PHP 函数或作为您自己的自定义函数)还允许您对它们使用函数式编程的全部功能,例如,通过从它们创建部分函数或将它们组合成组合函数,这些主题将在本章的后面讨论。
递归函数
递归函数只是一个调用自身的函数,任何类型的函数(命名、匿名或闭包)都可以是递归的。首先,我们来看几个递归函数的例子,看看它们为什么有用。然后,您将了解如何使用递归实现程序控制功能。
基本递归
让我们直接看一个例子。您有一个购物清单,存储为一个 PHP 数组(参见清单 3-3 )。
<?php
$shopping = [
"fruits" => [ "apples" => 7, "pears" => 4, "bananas" => 6 ],
"bakery" => [ "bread" => 1, "apple pie" => 2],
"meat" => [ "sausages" => 10, "steaks" => 3, "chorizo" => 1 ]
];
Listing 3-3.
shopping_list1.php
这是一个多维数组;在顶层,你有食物“组”(水果、面包、肉),每一组都是一系列的项目(苹果、梨、香蕉等)。)以及您想要购买的每种产品的数量。假设您想知道您的列表中总共有多少项。清单 3-4 是对项目进行计数的典型过程方式(输出如清单 3-5 所示)。
<?php
require('shopping_list1.php');
$total = 0;
foreach ($shopping as $group) {
foreach ($group as $food => $count) {
$total += $count;
}
}
echo "Total items to purchase : $total\n";
Listing 3-4.
foreach.php
Total items to purchase : 34
Listing 3-5.foreach-output.txt
您使用两个foreach
循环,第一个循环遍历食物组,第二个循环遍历组中的每一种食物,并将计数加到总数中。那是完成任务的一个完美的好方法。然而,对于挑剔的孩子来说,简单地买七个苹果是不够的。四个必须是红苹果,三个必须是绿苹果,否则战争就会爆发。所以,让我们改变列表,将苹果条目分解成一个嵌套的数组(见清单 3-6 )。
<?php
$shopping = [
"fruits" => [ "apples" => [ "red" => 3, "green" => 4], "pears" => 4, "bananas" => 6 ],
"bakery" => [ "bread" => 1, "apple pie" => 2],
"meat" => [ "sausages" => 10, "steaks" => 3, "chorizo" => 1 ]
];
Listing 3-6.
shopping_list2.php
现在对新列表运行foreach.php
脚本(参见清单 3-7 )。
PHP Fatal error: Uncaught Error: Unsupported operand types in foreach.php:11
Stack trace:
#0 {main}
thrown in foreach.php on line 11
Listing 3-7.foreach2-output.txt
新的apples
子阵列导致了一个问题。当脚本到达这个新数组时,它假设它将是一个数值,并尝试将它添加到$total
,导致 PHP 抛出一个错误。这种情况可以通过添加第三个foreach
循环来补救,但是这样你就需要转换所有其他的食物(梨、香蕉等等)。)转换为数组。当有人坚持认为红苹果需要进一步细分为 Gala 和 Braeburn 品种时会发生什么?这种构建代码的方式很明显是脆弱的,并且随着手头任务的发展很难维护。相反,您需要的是一段代码,它可以遍历任意深度的数组,并将所有“叶节点”(像bread
这样包含单个值而不是更多值的数组的元素)相加,无论它在哪里找到它们。这就是递归可以帮助我们的地方。参见清单 3-8 和清单 3-9 。
<?php
function count_total ($list) {
# we start like before, with a variable to hold our total
$total = 0;
# and then we loop through each value in our array
foreach ($list as $food => $value) {
# for each value in the array, which check if it
# is infact another array ...
if (is_array ($value)) {
# ... in which case we call *this* function
# on the new (sub)array, and add the result
# to the $total. This is the recursive part.
$total += count_total ($value);
} else {
# ... or if it's just a plain old value
# we add that straight to the total
$total += $value;
}
}
# once we've finished the foreach loop, we will have
# added ALL of the values of the array together, and
# also called count_total() on all of the sub-arrays
# and added that to our total (and each of those
# calls to count_total() will have done the same on
# any sub-arrays within those sub-arrays, and so on)
# so we can return the total.
return $total;
};
# Let's call it on our original shopping list
require('shopping_list1.php');
echo "List 1 : ".count_total($shopping)."\n";
# and then on the list with the apples sub-array
require('shopping_list2.php');
echo "List 2 : ".count_total($shopping)."\n";
# and finally on a new list which has sausages broken
# into pork and beef, with pork broken down to a third
# level between chipolatas and cumberland sausages.
require('shopping_list3.php');
echo "List 3 : ".count_total($shopping)."\n";
Listing 3-8.
recursive.php
List 1 : 34
List 2 : 34
List 3 : 34
Listing 3-9.recursive-output.txt
正如你所看到的,无论你如何将你的列表分成数组和子数组,相同的函数总是到达数组的每一部分。你可能需要一段时间来理解函数如何调用自己。理解这一点的关键是,每次你调用一个函数时(无论是否来自函数本身),都会在内存中创建一个函数状态的新“副本”。这意味着该函数的每次调用都与同一函数的其他调用有效地分离开来。当它们被调用时,每个函数调用的状态被放入内存中的调用堆栈中,查看调用堆栈可以帮助您可视化递归。为了演示,清单 3-10 (它的输出在清单 3-11 中)是一个简单的递归函数,它将所有数字相加到给定的数字(例如,对于sum(3)
,它做 3+2+1)。每次(递归地)调用函数时,debug_print_backtrace()
行将打印出调用堆栈。
<?php
function sum($start) {
echo "---\n";
debug_print_backtrace();
if ($start < 2) {
return 1;
} else {
return $start + sum($start-1);
}
}
echo "The result is : ".sum(4);
Listing 3-10.
recursive_stack.php
---
#0 sum(4) called at [recursive_stack.php:17]
---
#0 sum(3) called at [recursive_stack.php:13]
#1 sum(4) called at [recursive_stack.php:17]
---
#0 sum(2) called at [recursive_stack.php:13]
#1 sum(3) called at [recursive_stack.php:13]
#2 sum(4) called at [recursive_stack.php:17]
---
#0 sum(1) called at [recursive_stack.php:13]
#1 sum(2) called at [recursive_stack.php:13]
#2 sum(3) called at [recursive_stack.php:13]
#3 sum(4) called at [recursive_stack.php:17]
The result is : 10
Listing 3-11.recursive_stack-output.txt
在清单 3-11 中的堆栈跟踪中,#0
是最近被调用的函数,你可以看到每次通过这个函数调用同一个函数时是如何将下一个数字加到总数中的。
递归是好的,但是你必须小心确保你的递归循环会在某个时候终止。考虑上市 3-12 和上市 3-13 。
<?php
ini_set('memory_limit','1G');
function forever() {
forever();
}
forever();
Listing 3-12.
forever.php
PHP Fatal error: Allowed memory size of 1073741824 bytes exhausted (tried to allocate 262144 bytes) in forever.php on line 6
Listing 3-13.forever-output.txt
函数forever()
每次被调用时都递归地调用自己;没有边界检查或其他机制导致它返回。正如我前面提到的,每次调用一个函数,都会在内存中创建一个副本,在这种情况下,由于没有办法退出函数,每次调用都会消耗越来越多的内存,并且永远不会通过退出函数来释放内存。注意,我用ini_set
为脚本显式设置了内存限制。与 web 脚本不同,PHP CLI 脚本默认没有内存限制。如果没有限制,这个脚本可能会耗尽所有可用的内存,从而使您的机器瘫痪。
调用函数时,函数中使用的每个变量、每个调试语句以及分配的其他资源都会占用宝贵的内存。调用一次,它可能加起来不多,但是递归调用数百或数千次,它可以很快成为一个问题。因此,您应该总是试图将递归函数中的每种状态形式保持在绝对最小值。
尾递归是递归的一种形式,递归调用是在函数的最后部分进行的。在许多语言中,编译器可以优化尾部递归,因为它不需要“堆栈帧”来让编译器存储状态并返回。不幸的是,PHP 虚拟机不提供这样的优化,所以我不会详细讨论尾部递归。在下一章中,你将会看到“蹦床”,这是一种通过自动将递归函数展平成循环来获得类似优化的方法。
实现递归函数
在“映射、过滤和减少”部分,您创建了一个脚本来生成美味的新食谱。您通过使用array_map
来组合两个大小为n
的数组的元素,生成了您的候选食谱,并给出了作为输出的n
组合食谱。但是,如果你想扩大你的烹饪视野,并根据$ingredients
和$dish_types
的每种可能组合(即n
× n
组合)获得一个候选列表,该怎么办呢?你可以用两个嵌入的foreach
循环来实现,如清单 3-14 和清单 3-15 所示。
<?php
$ingredients = [
"cod", "beef", "kiwi", "egg", "vinegar"
];
$dish_types = [
"pie", "smoothie", "tart", "ice cream", "crumble"
];
$all_recipes = [];
foreach ($ingredients as $ingredient) {
foreach ($dish_types as $dish) {
$all_recipes[] = $ingredient.' '.$dish;
}
}
print_r($all_recipes);
Listing 3-14.
all_recipes.php
Array
(
[0] => cod pie
[1] => cod smoothie
[2] => cod tart
[3] => cod ice cream
[4] => cod crumble
[5] => beef pie
[6] => beef smoothie
[7] => beef tart
[8] => beef ice cream
[9] => beef crumble
[10] => kiwi pie
[11] => kiwi smoothie
[12] => kiwi tart
[13] => kiwi ice cream
[14] => kiwi crumble
[15] => egg pie
[16] => egg smoothie
[17] => egg tart
[18] => egg ice cream
[19] => egg crumble
[20] => vinegar pie
[21] => vinegar smoothie
[22] => vinegar tart
[23] => vinegar ice cream
[24] => vinegar crumble
)
Listing 3-15.all_recipes-output.txt
这些foreach
循环非常具体;它们接受特定的变量作为输入,因为它们不是函数的一部分,所以不能在其他地方重用。而print_r
有点要么全有要么全无;也许您只想打印列表中的第一个n
项目。那么,让我们看看如何让它更实用。
您将创建两个函数。第一个是清单 3-14 中所示的foreach
循环的递归版本。第二个将是更灵活版本的print_r
呼叫。您将把它们保存在一个单独的可重用的 PHP 文件中,然后从主脚本中调用它们。参见清单 3-16 ,清单 3-17 ,清单 3-18 。
<?php
function combine($a,$b) {
$combinations = [];
if (is_array($a)) {
foreach ($a as $i) {
$combinations = array_merge( $combinations, combine($i, $b) );
}
} else {
foreach ($b as $i) {
$combinations[] = $a.' '.$i;
}
}
return $combinations;
}
function print_first($items, $count) {
for ($counter=0; $counter<$count; $counter++) {
echo "$counter. ${items[$counter]} \n";
}
}
Listing 3-16.recipe_functions.php
<?php
require_once('recipe_functions.php');
$ingredients = [
"cod", "beef", "kiwi", "egg", "vinegar"
];
$dish_types = [
"pie", "smoothie", "tart", "ice cream", "crumble"
];
$all_recipes = combine($ingredients, $dish_types);
print_first($all_recipes, 5);
Listing 3-17.all_recipes_recursive.php
Showing 5 of 25 items:
1\. cod pie
2\. cod smoothie
3\. cod tart
4\. cod ice cream
5\. cod crumble
Listing 3-18.all_recipes_recursive_output.txt
那么,为什么这比原来的命令式foreach
版本要好呢?考虑一下,如果您需要一个不同的$ingredients
列表结构会发生什么。例如,如果您更换了您的配料供应商,并且他们的数据馈送的结构不同,那么您会看到清单 3-19 和清单 3-20 。
<?php
require_once('recipe_functions.php');
$ingredients = [
["ham", "beef"],
["apple", "kumquat"],
"vinegar"
];
$dish_types = [
"pie", "smoothie", "tart", "ice cream", "crumble"
];
$all_recipes = combine($ingredients, $dish_types);
print_first($all_recipes, 11);
Listing 3-19.
new_ingredients.php
Showing 11 of 25 items:
1\. ham pie
2\. ham smoothie
3\. ham tart
4\. ham ice cream
5\. ham crumble
6\. beef pie
7\. beef smoothie
8\. beef tart
9\. beef ice cream
10\. beef crumble
11\. apple pie
Listing 3-20.new_ingredients-output.txt
如您所见,递归的combine
函数不需要任何改变来处理$ingredients
数组的新结构,并且递归地向下进入每个子数组。
除了前面讨论的好处,递归函数还有助于确保程序的正确性。通过经常消除经由计数器等明确保持“循环”状态的需要,引入逐个错误等的机会大大减少。
部分功能
在第一章中,你看到了函数式编程是如何体现 OOP 的坚实原则的。其中一个原则是接口分离原则(ISP ),这意味着只有完成当前任务所必需的参数才是需要传递给函数的参数。
考虑上一节recipe_functions.php
中的print_first
函数。它需要两个参数,要打印的项目数组和要打印的项目数量。通常,需要这两个参数是合理的,因为对于给定的任务,这两个参数通常会有所不同。但是,如果你正在写一个新的网站,theTopFiveBestEverListsOfStuff.com,在那里你只想打印出给你的任何列表的前五项。你当然可以在你的脚本中反复输入print_first($list, 5)
。但是,当前五名最佳榜单的市场变得饱和,你需要进入前十名最佳市场时,你需要找到所有这些 5,并用 10 取代它们。如果你不小心把 5 输错成了 4,或者把 10 输错成了 1,你会在一个下午失去一半的市场份额。
当然,你可以用一个变量代替 5,比如说$count
,然后在需要的时候设置$count = 10
。但是在全局范围内这样做意味着额外的工作,以确保它对其他函数范围内的调用可用,并且当另一个程序员偶然在某个地方使用$count
作为循环计数器时,奇怪的错误就会比比皆是。
部分函数给了你一个解决这些问题的方法。部分函数是新的函数,它采用现有的函数,并通过将一个值绑定到一个(或多个)参数来减少其 arity。换句话说,部分函数通过将其一个或多个参数固定为特定值来制作现有函数的更具体版本,从而减少调用它所需的参数数量。让我们创建一个打印前五名网站列表的部分函数。见清单 3-21 和清单 3-22 。
<?php
require_once('print_first.php');
# Some data ...
$best_names = ["Rob", "Robert", "Robbie", "Izzy", "Ellie", "Indy",
"Parv", "Mia", "Joe", "Surinder", "Lesley"];
# Calling the function in full
print_first($best_names, 5);
# Now let's define a partial function, print_top_list, which
# binds the value 5 to the second parameter of print_first
function print_top_list($list) {
print_first($list, 5);
};
# Calling the new partial function will give the same
# output as the full function call above.
print_top_list($best_names);
Listing 3-21.
top_five.php
Showing 5 of 11 items:
1\. Rob
2\. Robert
3\. Robbie
4\. Izzy
5\. Ellie
Showing 5 of 11 items:
1\. Rob
2\. Robert
3\. Robbie
4\. Izzy
5\. Ellie
Listing 3-22.top_five-output.txt
现在,您可以在整个网站上愉快地使用print_top_list
分部函数,因为您知道 a)您可以随时在一个单独的中心位置将数字 5 更改为 10,b)您仍然可以从对底层print_first
函数的任何更新或更改中受益,c)您仍然可以在任何其他脚本中使用任何数字作为第二个参数来直接调用print_first
函数,这些脚本恰好使用相同的函数,但需要不同的数字。
虽然这展示了部分功能的好处,但是您手动创建它的方式有点笨拙,并且不可重用。所以,让我们成为真正的函数式程序员,创建一个函数来创建你的部分函数!我在第二章讲过高阶函数;提醒一下,这些函数可以将其他函数作为输入和/或将它们作为输出返回。您将定义一个名为partial
的函数,它接受一个函数和一个或多个绑定到它的参数,并给出一个现成的部分函数供您使用。参见清单 3-23 ,清单 3-24 ,清单 3-25 。
<?php
# Our function to create a partial function. $func is
# a "callable", i.e. a closure or the name of a function, and
# $args is one or more arguments to bind to the function.
function partial($func, ...$args) {
# We return our partial function as a closure
return function() use ($func, $args) {
# The partial function we return consists of
# a call to the full function using "call_user_func_array"
# with a list of arguments made up of our bound
# argument(s) in $args plus any others supplied at
# calltime (via func_get_args)
return call_user_func_array($func, array_merge($args, func_get_args() ) );
};
}
# The partial function generator above binds the given
# n arguments to the *first* n arguments. In our case
# we want to bind the *last* argument, so we'll create
# another function that returns a function with the
# arguments reversed.
function reverse($func) {
return function() use ($func) {
return call_user_func_array($func,
array_reverse(func_get_args()));
};
}
Listing 3-23.
partial_generator.php
<?php
require_once('print_first.php');
require_once('partial_generator.php');
$foods = ["mango", "apple pie", "cheese", "steak", "yoghurt", "chips"];
$print_top_five = partial(reverse('print_first'), 5);
$print_top_five($foods);
$print_best = partial(reverse('print_first'), 1);
$print_best($foods);
Listing 3-24.partial.php
Showing 5 of 6 items:
1\. mango
2\. apple pie
3\. cheese
4\. steak
5\. yoghurt
Showing 1 of 6 items:
1\. mango
Listing 3-25.partial-output.txt
这个例子使用了一个命名函数,而不是一个闭包,尽管我前面已经说过命名函数。这是为了本书的范围而特意设计的;你以后还会用到它,在你编写的简单程序中,把它作为一个命名函数使用意味着你不需要在每个你想调用它的函数中用到它。在你的程序中,如果对你有好处的话,你可以把它改成闭包。
如您所见,分部函数生成器允许您以可重用的方式创建多个分部函数,并且您创建了两个不同的分部函数($print_top_five
和$print_best
)。您可以使用此函数将任何函数的 arity 减少任意数量。考虑清单 3-26 中的函数,它的 arity 为 4,你将把它减 2。清单 3-27 显示了输出。
<?php
require_once("partial_generator.php");
$concatenate = function ($a, $b, $c, $d) {
return $a.$b.$c.$d;
};
echo $concatenate("what ", "is ", "your ", "name\n");
$whatis = partial($concatenate, "what ", "is ");
echo $whatis("happening ", "here\n");
Listing 3-26.
concatenate.php
what is your name
what is happening here
Listing 3-27.concatenate-output.txt
部分函数帮助您将功能分解成单一用途、可重用和可维护的功能。它们允许您在几个不同的任务之间共享更广泛的“整体”功能的核心功能,同时仍然受益于完整功能中的功能集中化。它们还允许你(如果你愿意的话)接近纯数学函数的奇偶校验,纯数学函数只接受一个参数。在下一章中,您将会看到 currying,尽管我的重点是食物,但它并不是一种将印度菜肴功能化的方法,而是一种将多实参函数自动分解成一系列单实参函数的方法。
函数表达式
函数式编程倾向于使用“函数表达式”进行程序控制,而不是传统的命令式控制结构,你已经间接看到了一些这样的例子。您可以使用已经探索过的技术来组合一些更有用的表达式。
一些最容易转换和理解的例子是数字函数。毕竟函数式编程来源于数学。在许多语言中,函数inc
和dec
用于增加和减少整数。在 PHP 中,你习惯于使用++
和--
操作符,但是没有理由不使用名为inc
和dec
的函数来编写自己的函数表达式。您可能想创建如清单 3-28 所示的这些函数来实现这一点(输出如清单 3-29 所示)。
<?php
function inc($number) {
$number++;
return $number;
}
function dec($number) {
$number--;
return $number;
}
var_dump( inc(3) );
var_dump( dec(3) );
Listing 3-28.inc_dec.php
int(4)
int(2)
Listing 3-29.inc_dec-output.txt
这是完全正确的,但是让我们考虑一种不同的方法,使用您之前看到的部分函数技术。见清单 3-30 和清单 3-31 。
<?php
require_once('partial_generator.php');
# First define a generic adding function
function add($a,$b) {
return $a + $b;
}
# Then create our inc and dec as partial functions
# of the add() function.
$inc = partial('add', 1);
$dec = partial('add', -1);
var_dump( $inc(3) );
var_dump( $dec(3) );
# Creating variations is then a simple one-liner
$inc_ten = partial('add', 10);
var_dump( $inc_ten(20) );
# and we still have our add function. We can start
# to build more complex functional expressions
$answer = add( $inc(3), $inc_ten(20) );
var_dump ( $answer );
Listing 3-30.inc_dec_partial.php
int(4)
int(2)
int(30)
int(34)
Listing 3-31.inc_dec_partial-output.txt
请注意,您可以使用这些技术随意混合和匹配命名函数和匿名函数。最初只需付出一点额外的努力,就可以获得更大的灵活性和更容易地创建其他派生函数。另一个例子可能是根据用例创建功能版本的能力。例如,你和我可能认为一打是 12,但对面包师来说是 13。见清单 3-32 和清单 3-33 。
<?php
require_once('partial_generator.php');
# Define a multiply function
function multiply($a,$b) { return $a * $b;}
# And then create two ways to count in
# dozens, depending on your industry
$programmers_dozens = partial('multiply', 12);
$bakers_dozens = partial('multiply', 13);
var_dump( $programmers_dozens(2) );
var_dump( $bakers_dozens(2) );
Listing 3-32.dsl.php
int(24)
int(26)
Listing 3-33.dsl-output.txt
这种创建描述它们将要做什么的函数,而不是详细说明如何做的能力,是函数式编程非常适合创建特定领域语言(DSL)的特性之一。DSL 是为特定应用“领域”(例如,特定行业或特定类型的软件)定制的语言或现有语言的改编。
操作组合
您已经讨论了一种通过减少现有函数的 arity 来创建新函数的方法,但是如果您想通过组合多个现有函数来创建新函数,该怎么办呢?您可以使用中间变量一个接一个地调用函数,将输出从一个传递到下一个。或者,您可以通过直接使用一个函数作为下一个函数的参数来将它们链接在一起。这是功能组合的一种形式,而且总是有更好的“功能”方式来实现。
假设你有一个秘密公式,可以计算出制作世界上最好的芒果冰淇淋的最佳温度。该公式将你正在使用的芒果数量(比如说 6 个),翻倍(12),求反(-12),再加上 2(-10°C)。您需要将这个公式作为一个函数嵌入到运行冰淇淋制造机的 PHP 软件中。然而,你也做其他口味的冰淇淋,每一种都有自己独特的配方。因此,你需要从一组可重复使用的基本数学函数开始,将它们组合成一个专门针对芒果的公式,同时仍然给自己留有空间,以便稍后轻松实现草莓冰淇淋的公式。一种方法是将几个函数组合成一个mango_temp
函数,如清单 3-34 和清单 3-35 所示。
<?php
function double($number) { return $number * 2; };
function negate($number) { return -$number; };
function add_two($number) { return $number + 2; };
function mango_temp ($num_mangos) {
return add_two(
negate (
double (
$num_mangos
)
)
);
};
echo mango_temp(6)."°C\n";
Listing 3-34.
sums1.php
-10°C
Listing 3-35.sums1-output.txt
那很管用,但是读起来不太直观。因为每个函数都嵌套在前一个函数中,所以您实际上必须从右向后读,才能理解执行的顺序。纯函数式语言通常有一个语法或函数,像这样将函数组合在一起,但是以一种更容易阅读的方式。PHP 没有,但不用担心,因为创建自己的脚本很容易(参见清单 3-36 )。
<?php
# This is a special function which simply returns it's input,
# and is called the "identity function" in functional programming.
function identity ($value) { return $value; };
# This function takes a list of "callables" (function names, closures etc.)
# and returns a function composed of all of them, using array_reduce to
# reduce them into a single chain of nested functions.
function compose(...$functions)
{
return array_reduce(
# This is the array of functions, that we are reducing to one.
$functions,
# This is the function that operates on each item in $functions and
# returns a function with the chain of functions thus far wrapped in
# the current one.
function ($chain, $function) {
return function ($input) use ($chain, $function) {
return $function( $chain($input) );
};
},
# And this is the starting point for the reduction, which is where
# we use our $identity function as it effectively does nothing
'identity'
);
}
Listing 3-36.
compose.php
要了解如何使用它,请检查 mango 冰淇淋脚本的新版本,如清单 3-37 所示。
<?php
include('compose.php');
function double($number) { return $number * 2; };
function negate($number) { return -$number; };
function add_two($number) { return $number + 2; };
$mango_temp = compose(
'double',
'negate',
'add_two'
);
echo $mango_temp(6)."°C\n\n ";
print_r ($mango_temp);
Listing 3-37.
sums2.php
我希望你同意这是更容易阅读和遵循执行链。因为mango_temp
函数是一个闭包,所以可以使用print_r
来查看compose
函数创建的结构(参见清单 3-38 )。
-10°C
Closure Object
(
[static] => Array
(
[chain] => Closure Object
(
[static] => Array
(
[chain] => Closure Object
(
[static] => Array
(
[chain] => identity
[function] => double
)
[parameter] => Array
(
[$input] => <required>
)
)
[function] => negate
)
[parameter] => Array
(
[$input] => <required>
)
)
[function] => add_two
)
[parameter] => Array
(
[$input] => <required>
)
)
Listing 3-38.sums2-output.txt
您可以在链的起点(输出的中间)看到 identity 函数,每个连续的函数依次作为每个“链”闭包的属性。
在第一章中,你看到了一个函数型代码的例子。我不想在那个阶段引入复合函数,以免过早地把水搅浑。然而,现在你已经知道了合成,你可以重写这个例子,如清单 3-39 所示。
<?php
require_once('image_functions.php');
require_once('stats_functions.php');
require_once('data_functions.php');
require_once('compose.php');
$csv_data = file_get_contents('my_data.csv');
$make_chart = compose(
'data_to_array',
'generate_stats',
'make_chart_image'
);
file_put_contents('my_chart.png', $make_chart( $csv_data ) );
Listing 3-39.
example2.php
从这些例子中需要注意的一点是,您的compose
函数只适用于只有一个参数的函数。这是故意的,因为函数只能返回一个返回值。如果一个函数接受两个参数,那么compose
函数如何知道在哪里使用前一个函数调用的单个返回值呢?
您可以使用我已经介绍过的类似部分函数的技术来创建单 arity 函数,以便与compose
一起使用。当然,如果您需要在函数之间移动数据集,单个参数可以是数组或类似的数据结构。强制使用单个参数也有助于确保您的函数尽可能简单,并且在范围上尽可能有限。然而,能够使用接受多个参数的其他函数来组合一个函数通常是实用的(或者有时是必要的,如果您使用其他人的函数或代码的话)。函数式编程在这里也有涉及;您只需将该函数包装在另一个返回函数的函数中!清单 3-40 和清单 3-41 展示了如何使用 PHP 的本地str_repeat
函数(它有两个参数:一个字符串和重复它的次数),这应该会让事情变得更清楚一些。
<?php
include('compose.php');
# A function to format a string for display
function display($string) {
echo "The string is : ".$string."\n";
};
# Our function to wrap str_repeat.
# Note it takes one parameter, the $count
function repeat_str($count) {
# This function returns another (closure) function,
# which binds $count, and accepts a single parameter
# $string. Note that *this* returned closure is the
# actual function that gets used in compose().
return function ($string) use ($count) {
return str_repeat($string, $count);
};
};
# Now let's compose those two functions together.
$ten_chars = compose(
repeat_str(10),
'display'
);
# and run our composed function
echo $ten_chars('*');
Listing 3-40.
strrepeat.php
The string is : **********
Listing 3-41.strrepeat-output.txt
理解您在这个脚本中做了什么的关键是要认识到,当您在compose
语句中使用repeat_str(10)
时,那不是您正在传递的函数。在函数名后放入括号会立即执行该函数,并用返回值替换它本身。所以,你在compose
语句定义中调用repeat_str(10)
,而repeat_str(10)
返回的函数就是compose
实际接收的参数。repeat_str(10)
返回一个闭包,它接受一个参数(这是您的compose
函数所需要的)作为$string
,但是通过使用($count
)偷偷将第二个参数(10)绑定到其中。
当然,你不必这样做;举例来说,你可以开始创建部分函数(例如一个repeat_ten_times($string)
函数),但是在很多情况下,这是一个更实用的组合多元函数的方法。
结论
你现在开始写功能代码了。在这一章中,您了解了以“函数式”方式构造函数的各种方法,并了解了诸如递归和部分函数等技术如何让您编写更灵活的函数。您可以使用到目前为止已经看到的技术来创建其他常见的程序控制结构,您将在阅读本书的其余部分时看到这些内容。在下一章,你将开始学习一些更高级的函数式编程主题。
四、高级函数式技术
到目前为止,您已经对函数式编程风格及其带来的优势有了一些了解。你可以从今天开始使用这些技术,而不需要进一步阅读。不过,理想情况下,我已经激起了您的兴趣,让您更进一步,学习更多的函数式技术,以添加到您的程序员工具箱中。
在这一章中,你将会看到函数式编程的一些更高级的方面,这些方面将会让你以更加函数化的方式来构建你的 PHP 代码。这一章是“理论”的最后一节,在这本书的下一部分,你将开始讨论实际的例子。您将从 currying 开始,它将部分函数应用的概念扩展为一种将它们分解成低级版本的自动化方式。接下来,您将看到虚构的单子,它帮助您进行程序流控制,并允许您处理在现实世界中工作时会遇到的讨厌的副作用。在那之后,你会看到蹦床,这是一种控制递归的方法。最后,我将简单介绍一下使用类型声明的严格类型化和动态类型化,虽然严格来说这不是一个函数概念,但在某些方面是有用的(而在其他方面则不是)。
Currying 函数
在前一章中,您看到了部分函数的优点,我提到了一种自动化这种函数分解的方法。分解是通过固定一个或多个参数的值,将多元函数分解成具有较小签名的函数的行为。你将要看到的自动分解方法叫做 currying,它是以 Haskell Curry 的名字命名的,他的名字(字面上)遍布函数式编程!
Currying 确实与部分函数应用密切相关,一个 currying 函数乍一看很像您创建的部分函数生成器。然而,有一些微妙但重要的区别。也就是说,部分功能应用只是一种奉承(或者反过来,取决于你和谁说话),所以两者的好处是相似的。您选择使用哪一种取决于在您的情况下什么对您有效。
在部分函数生成器中,您接受一个函数,加上一个绑定到第一个参数的值,并返回一个签名少了一个参数的函数。在 currying 中,通过获取一个函数和一个或多个参数的列表,并将所有这些参数绑定到返回的新函数,可以使这一点更加灵活。到目前为止,currying 是类似的(如果更一般)。要获得函数的实际结果,您需要到达已经绑定和/或传入所有函数参数的点,然后函数将执行并返回值。
生成器返回的部分函数和 currying 函数(在这两种情况下都是闭包)是两者区别的关键。使用您看到的简单部分生成器,返回的函数是一个具有简化签名的函数(即,调用它所需的参数数量减少)。如果您想进一步减少它以创建另一个部分函数,您可以在返回的闭包上再次调用部分函数生成器。相比之下,curry 例程返回的闭包是一个独立的函数,可以自动进一步 curry。例如,如果您有一个有五个参数的函数,您通过修改两个参数来处理它,您将得到一个接受三个参数的闭包。如果您随后用一个或多个参数调用这个闭包,那么它不会在没有一组完整参数的情况下执行(正如前面显示的部分函数那样),它会自动搜索自己并返回一个接受两个参数的闭包(如果用另一个参数调用,它还能够进一步搜索自己)。将这与您的生成器中的部分函数进行对比;如果您向接受三个参数的部分函数提供一个参数,它将尝试使用减少的参数集执行,这通常会导致错误。
和往常一样,这个例子可能会更清楚。由于编写一个格式正确的 currying 函数并不容易,您将使用 Matteo Giachino 编写的名为php-curry
的库来帮助您。这可以在 GitHub 的 https://github.com/matteosister/php-curry
获得,可以通过 Composer 安装,也可以直接通过包含它来安装,如清单 4-1 和清单 4-2 所示。
<?php
include('Curry/Placeholder.php');
include('Curry/functions.php');
use Cypress\Curry as C;
# Let's make a function place an order with our chef
# for some delicious curry (the food, not the function)
$make_a_curry = function($meat, $chili, $amount, $extras, $where) {
return [
"Meat type"=>$meat,
"Chili hotness"=>$chili,
"Quantity to make"=>$amount,
"Extras"=>$extras,
"Eat in or take out"=>$where
];
};
# We think that everyone will want a mild Rogan Josh, so
# let's curry the function with the first two parameters
$rogan_josh = C\curry($make_a_curry, 'Lamb','mild');
# $rogan_josh is now a closure that will continue to
# curry with the arguments we give it
$dishes = $rogan_josh("2 portions");
# likewise $dishes is now a closure that will continue
# to curry
$meal = $dishes('Naan bread');
# and so on for meal. However, we only have 1 parameter
# which we've not used, $where, and so when we add
# that, rather than returning another closure, $meal
# will execute and return the result of $make_a_curry
$order = $meal('Eat in');
print_r( $order );
# To show that our original function remains unmutated, when
# we realize that actually people only want 1 portion of curry
# at a time, with popadoms, and they want to eat it at home, we
# can curry it again. This time, the parameters we want to bind
# are at the end, so we use curry_right.
$meal_type = C\curry_right($make_a_curry, 'Take out', 'Poppadoms', '1 portion');
$madrass = $meal_type('hot', 'Chicken');
print_r( $madrass );
# We could curry the function with all of the parameters
# provided, this creates a parameter-less closure but doesn't
# execute it until we explicitly do so.
$korma = C\curry($make_a_curry,
'Chicken', 'Extra mild', 'Bucket full', 'Diet cola', 'Eat in');
print_r($korma());
Listing 4-1.currying.php
Array
(
[Meat type] => Lamb
[Chili hotness] => mild
[Quantity to make] => 2 portions
[Extras] => Naan bread
[Eat in or take out] => Eat in
)
Array
(
[Meat type] => Chicken
[Chili hotness] => hot
[Quantity to make] => 1 portion
[Extras] => Poppadoms
[Eat in or take out] => Take out
)
Array
(
[Meat type] => Chicken
[Chili hotness] => Extra mild
[Quantity to make] => Bucket full
[Extras] => Diet cola
[Eat in or take out] => Eat in
)
Listing 4-2.currying-output.txt
Currying 提供了一种更简洁的方式来管理部分函数,特别是当您经常需要一个给定函数或一组部分函数的许多不同版本时。由于额外的 currying 代码,这种权衡在闭包中是一个更大的开销,但是这通常是一个无关紧要的考虑。您也失去了允许可选参数的能力(至少对于这里实现的 currying 来说是这样的);在所有参数都被赋值之前,该函数不会执行。
除了曲线拟合,还有相反的过程,通常称为去曲线拟合或去曲线拟合,它采用一组 n 个单参数函数,并将它们组合成一个 n 元函数。这通常应用有限,所以我不会在本书中涉及。
神秘的单子
单子是程序控制的一种通用形式,是现实世界副作用问题的解决方案(比如从文件中读取和打印到屏幕上),也可以说是函数式编程(或一般生活)中最难理解的概念之一。如果你查找关于单子的介绍性文章或视频,他们总是会首先告诉你,专家说你需要理解范畴定理、内函子等数学主题,可能还有其他相关的深奥概念。然后他们会说,实际上,你不需要理解这些,因为他们已经找到了一种简单易懂的方式来解释单子。然后,他们将开始讨论函子(数学函子,不是编程函子)、应用函子、幺半群等等,最终根据这些概念定义幺半群,并在此过程中失去 90%的读者。然后,他们会声称他们给出了一个简短的非技术概述,这里有一个完整的数学解释的链接,以帮助澄清这一切,在这一点上,每个人都放弃了单子,函数式编程,有时甚至是他们迄今为止的整个生活方式。所以我不会那么做。
著名的 JavaScript 教育家道格拉斯·克洛克福特在他的谷歌技术演讲“单子和生殖腺”中有一句名言:
“单子的诅咒是,一旦有人知道单子是什么以及如何使用它们,他们就失去了向其他人解释(它们)的能力。”
一种不那么轻率的说法是,也许你对单子是什么以及单子能做什么的理解通常会随着时间的推移而发展。当你已经完全掌握了它们的力量,并且这个概念已经完全“点击”时,很难记得所有那些导致理解它们如何工作的更小的“发现”时刻。
所以,我从问题的另一端开始。我不会试图从基本原理中推导出单子,我会告诉你单子是什么,你用单子做什么,并给你看一些例子。一旦你习惯了使用它们,你就会有自己的“哦,是的,我现在明白了”的时刻,即使你不明白背后的数学原理。当然,我也会在最后提供与数学相关的解释的必要链接。
什么是单子?
monad 是一种函数,它封装了值,并在 monad 的上下文中将函数应用于这些值。你可以把它们看作是处理状态的容器,不同类型的单子在状态上承担不同种类的工作。你能用单子做什么?
- 控制程序流(作为功能组合的一种形式)
- 封装副作用(把它们从你的纯函数中去掉,使它们仅仅是程序本身的一个效果)
- 降低代码复杂性(好吧,一旦你理解了单子本身)
虽然单子是“唯一的”函数,您可以用 PHP 函数实现它们,但是使用对象创建它们通常更方便,因为这给了您更多的灵活性(并且允许您有时用额外的帮助方法作弊)。这是大多数实现单子的 PHP 库采用的方法。请记住,方法本质上只是在对象的上下文中调用的一个函数(事实上,闭包本质上是一个具有单一方法的对象,该方法对封装在其上下文中的一组数据进行操作)。
那么,野外的单子长什么样?它将有两个关键方法。
- 一个构造函数方法,它接受一个值并创建一个“包装”该值的 monad 对象
- 一种“绑定”方法
- 接受一个函数(或其他可调用函数)作为输入
- 在前面提到的构造函数包装的值上调用它
- 返回通过对被调用函数的结果调用构造函数方法而创建的新 monad
如您所见,bind 方法是这里的关键。它接受一个函数,将其应用于存储在 monad 中的值(即状态),并返回一个新的 monad 对象,其结果(“新”状态)作为该新 monad 的值。它还具有三个关键的数学属性,这使它有别于其他恰好保护一个值并绑定一个函数的结构。稍后您将详细查看这些内容,并了解如何使用它们来测试某个东西实际上是否是单子。
是时候看一个例子了。您不用编写自己的 monad 类,而是使用 Anthony Ferrara 编写的名为 MonadPHP 的库(可在 GitHub 的 https://github.com/ircmaxell/monad-php
获得)。这是单子的一个简单的“玩具”实现,非常适合学习。我强烈建议看一下源代码,因为它写得很好,很容易理解。您可以用 Composer 安装它,或者只需要这些文件,正如我将展示的那样。您将首先查看清单 4-3 中的身份单子(输出如清单 4-4 所示),它唯一的工作是调用包装值上的传递函数。它不是很有用,但是演示了一个基本单子的结构和属性。
<?php
require('MonadPHP/Monad.php');
require('MonadPHP/Identity.php');
# Use the namespace
use MonadPHP\Identity;
# Define a couple of pure functions
$double = function ($n) { return $n*2; };
$add_ten = function ($n) { return $n+10; };
# Create a monad by calling the static unit method,
# with a value (33). The unit method is a constructor
# which checks if what we are passing in is already
# an instance of this monad, or create a new one if
# not by calling the _construct method to bind
# our value (33) and return a monad object
$monad_a = Identity::unit(33);
# Let's check it is an object of class MonadPHP\Identity
# encapsulating the value 33
var_dump( $monad_a );
# Now we bind one of our functions to the monad
$monad_b = $monad_a->bind($double);
# $monad_b should be a new monad object.
# Let's check that it is and that we haven't
# just mutated monad1
var_dump( $monad_a ); # should be the same as above
var_dump( $monad_b ); # should be a new monad encapsulating 66
# This library includes a helper method "extract" to
# get the encapsulated value back out of the monad
var_dump( $monad_b->extract() ); #66
# Let's bind that function again to the new monad...
$monad_c = $monad_b->bind($double);
var_dump( $monad_c->extract() ); #132
# ... and check that monad_b is unchanged
var_dump( $monad_b->extract() ); #66
# finally, bind the function again to monad_b,
# to demonstrate again that its encapsulated value
# isn't mutated.
$monad_d = $monad_b->bind($double);
var_dump( $monad_d->extract() ); #132
# Let's now repeatedly bind methods
# in a chain
$monad_e = $monad_d->bind($double) # *2
->bind($add_ten) # +10
->bind($add_ten); # +10
var_dump( $monad_e->extract() ); # 284
# and take a look at the returned monad_e,
# take note of the object identifier (#7)
var_dump( $monad_e );
Listing 4-3.monad.php
object(MonadPHP\Identity)#3 (1) {
["value":protected]=>
int(33)
}
object(MonadPHP\Identity)#3 (1) {
["value":protected]=>
int(33)
}
object(MonadPHP\Identity)#4 (1) {
["value":protected]=>
int(66)
}
int(66)
int(132)
int(66)
int(132)
int(284)
object(MonadPHP\Identity)#7 (1) {
["value":protected]=>
int(284)
}
Listing 4-4.monad-output.txt
在该示例的最后一部分,您将一组绑定调用链接在一起,这看起来很像您在前一章中看到的那种函数组合。虽然您确实可以以这种方式使用 identity 函数来组合函数,但是 monads 不仅仅是将前一个函数的输出传递给下一个函数的输入。看一下散列值(#3、#4 等。)在清单 4-4 的var_dump
输出中。这些是 PHP 在当前上下文中创建的对象的内部标识符(数字形式)。(它从#3 开始,因为#1 和#2 是你的纯函数$double
和$add_ten
,它们是闭包类型的对象。)具体来说,#3 对应$monad_a
,#4 是$monad_b
。你不用在它们上面使用var_dump
,但是$monad_c
会是#5,$monad_d
会是#6。#7 是monad_e
。为什么这很重要?它演示了每次您在单子上调用bind
时,您会完全获得另一个对象。您每次都在围绕您的边界值改变上下文(状态),而不只是像简单的合成那样传递它。这允许你创建一些奇特的单子,做有趣的(和有用的)事情,你马上就会看到。
但是您在$monad_d
上创建$monad_e
的绑定调用“链”是什么呢?你已经应用了三个函数,但是最后你只得到一个新的单子。事实上,您正在创建三个新的单子,并使用 PHP 的对象解引用将下一个函数绑定到上一个单子。一眨眼的功夫,单子就在“幕后”被创造出来,然后被销毁,这就是为什么你看不到它们的原因。如果你不相信,你可以添加一个不纯的echo('creating')
调用到身份构造器方法,你会看到它为每个绑定调用输出creating
。
这些是单子的基础。现在让我们做一些有用的事情。虽然您可以发明单子来做您能想到的任何事情,但是您会遇到一些常见的单子来解决常见的函数式编程问题并实现典型的函数式模式。在像 Haskell 这样的纯函数式语言中,单子很受欢迎,因为它们几乎是完成实际工作的唯一方法。你将会看到一些最常见的单子来展示这种结构的可能性和力量。
可能单子
在前面的例子中,Identity monad 刚刚调用了绑定函数,其值封装在 monad 中。Maybe monad 更进一步,在用它调用绑定函数之前添加了一个对值的测试。
“我称之为我的十亿美元错误。”不,那不是我在谈论写这本书。这是查尔斯·安东尼·理查德·霍尔爵士的话,他是参考文献的发明者。Null 虽然是一个很好的传递信息的工具,比如失败或者缺少值,但是它也有自己的问题。函数通常返回 null,表示没有实际值要返回,这通常是因为函数中存在错误或类似错误,或者传递给函数的参数有问题。这使得调用者能够判断是否是这种情况,例如,返回值是否合法地为 0、false、空数组或类似的值,如果 null 不存在,这些值可能会被用作错误代码。然后调用者可以测试是否为空,并适当地处理它。
要了解这为什么会有问题,请考虑您在函数组合示例中看到的函数链。如果其中一个函数返回 null 作为返回值,会发生什么?空值作为输入被送入链中的下一个函数,这意味着现在需要修改所有的函数来测试和处理空值,否则就会出现问题。向前迈进一步,使用 Maybe monad,您可以处理“可能”会失败或“可能”会正确工作的函数,而不必在每个函数中编写额外的代码来检查。Maybe monad 通过在每次调用一个函数后检查是否有 null 返回值来实现这一点,如果发现 null,就不调用下一个函数。让我们看一个例子(参见清单 4-5 和清单 4-6 ),然后我将讨论它是如何工作的以及为什么工作。您将再次使用 MonadPHP 库。
<?php
require('MonadPHP/Monad.php');
require('MonadPHP/Maybe.php');
use MonadPHP\Maybe;
# We'll use the shopping list array from the previous chapter.
# It's a nested array, and not all elements have the
# same level of nesting.
$shopping_list = [
"fruits" => [ "apples" => [ "red" => 3, "green" => 4], "pears" => 4, "bananas" => 6 ],
"bakery" => [ "bread" => 1, "apple pie" => 2],
"meat" => [ "sausages" =>
["pork" => ["chipolata" => 5, "cumberland" => 2], "beef" => 3],
"steaks" => 3, "chorizo" => 1 ]];
# Let's create some functions.
# This function takes a category (e.g. fruits) and returns either
# a) a closure that returns that category from the supplied list
# or
# b) null if the category doesn't exist.
$get_foods = function ($category) {
return function ($list) use ($category) {
echo("get_foods return closure called\n");
return isset($list[$category]) ? $list[$category] : null;
};
};
# This function does the same, except it returns a closure that returns
# the foods (e.g. apples, pears...) from the category (fruit), or null
$get_types = function ($food) {
return function ($category) use ($food) {
echo("get_types return closure called\n");
return isset($category[$food]) ? $category[$food] : null;
};
};
# and lastly another function of the same type to get the types of food
# (e.g. red, green) from the food, or null
$get_count = function ($type) {
return function ($types) use ($type) {
echo("get_count return closure called\n");
return isset($types[$type]) ? $types[$type] : null;
};
};
# Now let's create a Maybe monad, encapsulating our
# shopping list as its value.
$monad = Maybe::unit($shopping_list);
# We'll repeatedly bind our functions against it as
# we did in the previous example.
var_dump( $monad ->bind($get_foods('fruits'))
->bind($get_types('apples'))
->bind($get_count('red'))
->extract() # returns 3
);
# None of our closures test for null parameters, so what
# happens if we try to look for something that doesn't exist?
var_dump( $monad ->bind($get_foods('fruits'))
->bind($get_types('apples'))
->bind($get_count('purple')) # doesn't exist
->extract() # returns null
);
var_dump( $monad ->bind($get_foods('cheeses')) # doesn't exist
->bind($get_types('cheddar')) # doesn't exist
->bind($get_count('mature')) # doesn't exist
->extract() # returns null
);
var_dump( $monad ->bind($get_foods('bakery'))
->bind($get_types('pastries')) # doesn't exist
->bind($get_count('danish')) # doesn't exist
->extract() # returns null
);
Listing 4-5.maybe_monad.php
get_foods return closure called
get_types return closure called
get_count return closure called
int(3)
get_foods return closure called
get_types return closure called
get_count return closure called
NULL
get_foods return closure called
NULL
get_foods return closure called
get_types return closure called
NULL
Listing 4-6.maybe_monad-output.txt
(注意,如果你在 PHP 7 上运行这个,你会得到一个抛出的警告,因为这个库从 PHP 5 开始就没有更新过;您可以更改库中的绑定声明,也可以安全地忽略该警告。)
我在那里加入了一些不纯的echo
语句,这样你就可以看到闭包何时被调用。正如您所看到的,当您试图从数组中获取不存在的项时(因此您的闭包返回 null),链中的后续函数调用不会被执行,即使您将它们绑定到 monad。那么,这是如何工作的,为什么呢?在这种情况下,单位单子的功能如下:
- 首先创建一个 monad,将购物清单作为它的值。
- 然后将第一个函数绑定到它,它用购物清单值调用这个函数。
- 这将返回一个值(数组的请求部分或 null),该值将被放入一个新的 monad 对象中并返回。
- 然后像前面一样将下一个函数绑定到新的单子上,以此类推,直到所有的函数都被绑定。
与同一性单子的关键区别在于,在绑定函数的阶段,条件语句检查单子的值,如果是 null,它不调用函数,而是将 null 返回到新单子中。因此,只要链中的一个函数返回空执行,所有后续的函数都会被跳过,因为封装在每个单子中的值都是空的(并且不会改变,因为它是空的,所以没有一个函数可以被调用…!).每次仍然在链中创建单子,但是封装的值(null)只是沿着链传播。
因此,作为程序员,您仍然需要编写一个空检查,但只在链的末端编写一次,而不是针对每个单独的函数。这允许您编写更简单的函数,这些函数可以假设它们将获得有效值(或者至少非空值)作为输入进行操作。如果您的函数对它们的输入值有其他共同的要求(例如,它们总是需要一个整数作为输入),您可以很容易地构造类似的条件单子,以您想要的任何方式检查它们的封装值。
这种在每次函数调用时执行代码的能力是 monads 相对于您在前一章中看到的简单函数组合技术的优势之一。它被比作“可编程的分号”,因为 PHP(像许多编程语言一样)一个接一个地执行语句,语句之间用分号隔开。想象一下,如果你能让分号做点什么;这应该给你一个单子在这种情况下的力量的概念。
单子公理
正如我在单子介绍中提到的,单子必须遵守三个公理(或定律)才能归类为单子。理解这些对判断你使用的东西是否真的是单子是有用的。明确地说,一些类似单子的结构只有一两个数学属性,仍然非常有用,但是您需要格外小心,以确保它们以您想要的函数方式运行,以确保您的代码具有您期望的全功能代码的属性。
同样,我不会试图推导或解释这些公理的方法和原因,但我会以一种有用的方式呈现什么。我将用 PHP 将它们表示为伪代码,而不是用它们的数学符号。
单子公理 1
bind( unit($i), $func ) == $func( $i )
这意味着如果你将一个函数$func
绑定到一个用值$i
创建的单子上,这相当于直接在$i
上调用$func
。unit
是单子构造函数的常用名称。
单子公理 2
bind($monad, unit) == $monad
这说明如果你将构造器单元函数绑定到一个单子上,结果就等同于那个单子。
单子公理 3
bind ( bind($monad, $f1), $f2) == bind ($monad, function($i) {return bind($f1($i), $f2($i)})
现在这个才是最难的!
左边说,“将一个函数$f1
绑定到一个单子,将另一个函数$f2
绑定到结果单子。”
右边说这相当于取一个单子$monad
,绑定一个返回单子的函数,单子是绑定函数$f2($i)
应用于$function $f1($i)
的结果,其中$f1
是返回单子的函数。
如果您没有遵循或理解任何公理(尤其是最后一条),也不要担心。在实践中,如果你需要使用它们,它总是作为一个测试,你可以直接应用它们。如果你真的想更好地理解它们,请看本章后面的“进一步阅读”部分。
测试单子公理
所以,让我们看看这个身份单子到底是不是真的单子(见清单 4-7 和清单 4-8 )。一个简单的测试是创建一个测试值和函数,创建一个单子,然后采用前面的公理并用合适的 PHP 编写它们,测试每个公理的计算结果是否为真。
<?php
require('MonadPHP/Monad.php');
require('MonadPHP/Identity.php');
use MonadPHP\Identity;
# 1\. bind( unit($i), $func ) == $func( $i )
// define some test variables and functions
$i = 10;
$func = function ($i) { return $i*2; };
// create a new monad to test
$monad = Identity::unit($i);
// see if the 1st Axiom holds (should output true)
var_dump ( $monad->bind($func)->extract() == $func($i) );
# 2\. bind($monad, unit) == $monad
// and see if the 2nd Axiom also holds
var_dump ( $monad->bind(Identity::unit) == $monad );
# 3\. bind ( bind($monad, $f1), $f2) ==
# bind ($monad, function($i) { return bind( $f1($i), $f2($i) } )
// create some more test functions
$f1 = function ($i) { return Identity::unit($i); }; // returns a monad
$f2 = function ($i) { return $i*6; };
// and see if Axiom 3 holds
var_dump (
$monad->bind($f1)->bind($f2) ==
$monad->bind(function ($i) use ($f1, $f2)
{ return $f1($i)->bind($f2); }
)
);
Listing 4-7.monad_test.php
bool(true)
bool(true)
bool(true)
Listing 4-8.monad_test-output.txt
嗯,看起来都不错。功能稍微强一点的单子怎么样?你将以完全相同的方式处理这个问题(参见清单 4-9 和清单 4-10 )。
<?php
require('MonadPHP/Monad.php');
require('MonadPHP/Maybe.php');
use MonadPHP\Maybe;
# 1\. bind( unit($i), $func ) == $func( $i )
// define some test variables and functions
$i = 10;
$func = function ($i) { return $i*2; };
// create a new monad to test
$monad = Maybe::unit($i);
// see if the 1st Axiom holds (should output true)
var_dump ( $monad->bind($func)->extract() == $func($i) );
# 2\. bind($monad, unit) == $monad
// and see if the 2nd Axiom also holds
var_dump ( $monad->bind(Maybe::unit) == $monad );
# 3\. bind ( bind($monad, $f1), $f2) ==
# bind ($monad, function($i) { return bind( $f1($i), $f2($i) } )
// create some more test functions
$f1 = function ($i) { return Maybe::unit($i); }; // returns a monad
$f2 = function ($i) { return $i*6; };
// and see if Axiom 3 holds
var_dump (
$monad->bind($f1)->bind($f2) ==
$monad->bind(function ($i) use ($f1, $f2)
{ return $f1($i)->bind($f2); }
)
);
Listing 4-9.maybe_test.php
bool(true)
bool(true)
bool(true)
Listing 4-10.maybe_test-output.txt
这也通过了所有三个公理试验。当然,这不是一个全面的测试;公理必须适用于传递给 monad 的任何和所有(构造良好的)输入/函数,但是如果您想要更彻底地测试您创建的 monad 的行为,它应该会为您提供一些关于如何进行的线索。
其他有用的单子
您已经了解了什么是单子,以及它如何成为组合函数的通用方式。在本章的介绍中,我说过我将解释如何使用单子来解决函数式编程中的一些问题,例如处理否则会被归类为副作用的操作。这就是我在这一节要做的事情。
首先,你会看到一个常见的单子,称为作家单子。编写软件时的一个常见任务是随着程序的进展将信息记录到磁盘上。这可以是从调试信息或审计日志到跟踪信息和事务记录的任何内容。通常,发送到日志的消息在被记录时被直接或通过调用日志记录函数写入磁盘。在函数式程序中,你可以创建一个日志记录函数,但是如果它向磁盘写入任何东西,它就不是一个纯粹的函数。如果没有的话,它作为一个日志功能就没有多大用处了!
用于处理副作用的一种技术(或者更好的称呼是妥协)是将所有“不纯的”行为推到程序运行的末尾。通过这种方式,程序的大部分是“适当的”功能性的,能够被完全测试,并且容易推理,至少如果混乱不纯的部分有问题,它都在一个地方,并且你知道它发生在哪里。
那么,如何将所有的日志写到程序的末尾呢?一种方法是创建一个全局变量来收集要记录的信息,最后将它一次性写入磁盘。然而,正如我已经讨论过的,在函数式编程中,全局状态通常被认为是一个坏主意,因为您在函数流之外引入了状态,而您在任何时候都不能(容易地)推理或确信这一点。另一种方法是在进行过程中沿着函数链传递日志信息;每个函数都可以从上一个函数的输入中获取“日志”,添加自己的日志记录,并将日志作为其返回值的一部分传递给链中的下一个函数,只有在函数链中有最终值时才写入磁盘。这将是一个非常好的函数方式,除了这意味着您需要改变每个函数的签名来接受传递这些额外的数据,可能使用数组或类似的东西来保持日志信息和实际函数输出的分离和组织。你可能已经猜到了,问题的答案是作家莫纳德。
Writer monad 提供了一种方法,可以像您期望的那样编写和组合函数,而不必改变它们接受的参数。在后台,编写器 monad 用“写入的”信息(例如,在这种情况下,要记录的字符串)构建单独的数据结构,并且在 monad 函数链的末端,返回两个值(正常返回值和日志数据)。为了在实践中演示这一点,这次您将使用一个不同的单子库,名为php-fp-writer
,由 Tom Harding 编写,您可以从 https://github.com/php-fp/php-fp-writer
下载。你可以用 Composer 安装它,或者简单地包含我在这里展示的文件。
但是,在实现 Writer 示例之前,您需要创建一个称为幺半群的结构。在运行函数链时,monad 中需要一个结构来“收集”和保存日志消息(或者 Writer monad 将为您处理的任何数据),monad 本身处理实际的函数返回值,就像前面的例子一样。
什么是幺半群?它的两个关键特性是,它有一个“关联二元运算”和一个“单位元素”在某些方面,幺半群感觉有点像单子;它包装一个值(保持静态),应用一个函数,并返回一个新的幺半群作为输出,而不是对自身进行变异。数学家桑德斯·麦克兰恩在《工作的数学家》一书中说:
总之,X 中的幺半群只是 X 的内函子范畴中的幺半群,其乘积 X 被内函子的复合所取代,单位集被单位内函子所取代
对于不知道内函子是什么的外行人来说,解释这段引文,单子实际上只是更一般的幺半群的一个特例。像单子一样,你将创建单子作为对象。因为它们要简单得多,你将编写自己的幺半群类(见清单 4-11 )。您的对象将要执行的“关联二进制操作”是连接,在这种情况下,您将把(日志条目的)字符串连接到一个数组中。identity 元素是一个元素(或值),当对其他值执行操作时,这些值保持不变。对于串联(到一个数组中),identity 元素是一个空数组。在这种情况下,您不打算使用 identity 元素,所以为了清楚起见,您将省略它,但是如果您愿意,您可以添加一个empty()
方法,该方法返回一个以空数组作为其封装值的幺半群。
<?php
class Monoid {
public function __construct($value) {
$this->value = $value;
}
public function concat($to) {
return new Monoid(array_merge($to->value, $this->value));
}
};
Listing 4-11.monoid.php
您将使用幺半群作为添加和保存日志数据的结构。然后你要写你的“有用的”纯函数,所以下面的事情发生了:
- 返回值是一个写者单子。
- monad 是通过调用执行以下操作的
Writer::tell
静态方法创建的:- 创建一个附加了幺半群的编写器幺半群对象
- 绑定实际“有用”的函数,准备在主函数值上调用
这允许您使用 monad 的 chain 方法将函数链接在一起。
为了使这一点更清楚,请看清单 4-12 (以及清单 4-13 )。您将使用上一章中的冰淇淋温度示例,该示例将温度作为一个整数,作为函数链的返回值。您还将收到第二个返回值,这是一组准备记录到磁盘或屏幕上的语句。正如您将注意到的,这个库创建和链接 monads 的方法与您看到的第一个库略有不同,这表明有多种方法可以为 monad cat 换肤,但它应该足够熟悉,以便理解正在发生的事情。如果你愿意,你总是可以用你之前看到的公理来测试以这种方式创建的单子,以确保你很高兴这就是你所使用的。
<?php
include('src/Writer.php');
include('monoid.php');
use PhpFp\Writer\Writer;
function double($number) {
$log = new Monoid(["Doubling $number"]);
return Writer::tell($log)->map(
function () use ($number)
{
return $number * 2;
}
);
};
function negate($number) {
$log = new Monoid(["Negating $number"]);
return Writer::tell($log)->map(
function () use ($number)
{
return -$number;
}
);
};
function add_two($number) {
$log = new Monoid(["Adding 2 to $number"]);
return Writer::tell($log)->map(
function () use ($number)
{
return $number + 2;
}
);
};
list ($mango_temp, $log) = double(6)->chain('negate')->chain('add_two')->run();
echo $mango_temp."°C\nLog :\n";
print_r($log->value);
Listing 4-12.
writer_monad.php
-10°C
Log :
Array
(
[0] => Doubling 6
[1] => Negating 12
[2] => Adding 2 to -12
)
Listing 4-13.writer_monad-output.txt
查看前面的输出,您可以看到您获得了正确的-10°c。您还获得了第二个数组,包含作为您调用的三个函数的结果的三个“log”字符串,您现在可以在您的“不纯”代码中使用它(例如,通过写入磁盘等)).这两个输出被包装到一个数组中,所以使用list
语言构造将数组分成两个变量($mango_temp
和$log
)。
关于前面的代码,需要注意的另一个有趣的地方是在函数链的末尾使用了run()
方法。如果您忽略这一点并运行脚本,您会发现您的链中没有一个函数被真正调用。这种类型的 monad 构建函数链,然后只在构建后“运行”它。这对于测试非常有用,是典型的单子,可以帮助您处理潜在的副作用,您将在下一种单子 IO 单子中看到这一点。
木卫一单子
在前面的例子中,Writer monad,你把所有不纯的操作推到脚本的末尾,把你想操作的信息收集到一个数组中,一直带着它直到所有纯函数完成,然后把它和你的脚本的主返回值一起返回,以处理写入磁盘或类似操作的讨厌的副作用。这是处理副作用的好方法,但是在很多情况下,等到代码结束才开始与外部系统对话是不现实的。例如,您可能需要从外部来源(API、文件、数据库)收集输入,这些来源会根据某个纯函数中途进行的计算以及其他函数的计算而变化。你需要另一个工具来利用不纯的动作,这个工具就是 IO monad。您将使用 Tom Harding 的php-fp-io
库,可从 GitHub 的 https://github.com/php-fp/php-fp-io
获得,它是早期php-fp-writer
的姐妹库,因此遵循相同的结构和风格。
看一看下面的代码。该模式在 Writer monad 示例中应该很熟悉,但是正如您将看到的,您调用了三个被认为是不纯的函数/语言构造(它们有副作用),您需要在进行过程中调用它们。
random_bytes
:这引入了来自外部状态源的值(在 Linux 上通常是/dev/urandom
),当用于创建函数的返回值时,显然意味着从给定的输入参数集你将无法确定返回值。- 这个函数(就像大多数文件系统函数一样)不能保证没有副作用,即使你没有从中读取未知的值。例如,如果文件或文件系统没有处于您期望的状态,就会产生错误和异常,因此您不能可靠地推断您的函数执行了预期的操作。
- 由于它只是输出到屏幕上(或网络服务器上),也许你认为 echo 不会出什么问题,不会对你的功能造成问题/副作用?考虑一下,如果你的脚本有一个名为
fclose(STDOUT)
的地方,或者STDOUT
流在你不知情的情况下从你的程序外部被关闭了;那么调用echo
会导致你的程序在没有警告的情况下终止。
当然,还有许多其他函数具有与 I/O 相关的不同类型的副作用,您可以使用它们来演示这里的原理。
那么,让我们来看看你的剧本。你得到 100 个随机字节,并把它们转换成一个十六进制字符串(为了更容易印在书里!),将它们写入一个名为random.txt
的文件,最后向屏幕输出一条消息,确认你已经完成了任务。但是实际上你要把它分成两个脚本。第一个是io_monad.php
,它设置了完成任务所需的所有函数,并创建了一个函数链。然后它“返回”最后一行的链。如果你没有见过像这样使用return
(例如,在函数之外),不用担心。您这样写是为了在第二个脚本中可以“包含”第一个脚本作为函数体。注意run_io.php
中 include 调用周围的括号;这些构成了一个立即调用的函数表达式(IIFE ),它执行 tin 上所说的内容:它立即调用括号内的代码,就好像它是一个被调用的函数一样。这是 PHP RFC 关于统一变量语法的一部分,是作为 PHP 7 的一部分引入的。因为第一个文件中的代码被作为函数调用,return
语句现在应该更有意义了!参见清单 4-14 ,清单 4-15 ,清单 4-16 ,清单 4-17 。
<?php
include('src/IO.php');
use PhpFp\IO\IO;
# Some functions that define how to create
# some other, impure functions
# Make a random string of hex characters from $length random bytes
$string_maker = function($length) {
return new IO( function () use ($length) {
return bin2hex(random_bytes($length));
}
);
};
# Write a string to $filename on disk
$file_writer = function($filename) {
return function ($string) use ($filename) {
return new IO( function () use ($filename,$string) {
file_put_contents($filename,$string);
}
);
};
};
# Send ($string) to STDOUT
$printer = function($string) {
return function () use ($string) {
return new IO( function () use ($string) {
echo($string."\n");
}
);
};
};
# Chain those functions together, and return the resulting
# monad
return $string_maker(100)
->chain($file_writer('random.txt'))
->chain($printer('All done'));
Listing 4-14.io_monad.php
<?php
# Start an IIFE
(
# Execute the io_monad.php file to get the monad
require('io_monad.php')
# At this stage, we have a monad full of functions
# that have not been called (and so haven't done)
# any "impure" work
# Finally call the unsafePerform() method on the monad to
# call the "impure" functions
)->unsafePerform();
Listing 4-15.run_io.php
All done
Listing 4-16.run_io-output.txt
935998b29780e9f8f56435120208f7196854f677a666abcc510fee8a7162d12f6d923e470b4373f232dfbb0bf1a9da28e9b8a3f84af15273fc516ccf74c493ebce3931922a59d83ba80d77cfc41e8c76ffd90d79d91e32bcf2fbdf15a85ec38b1c5186cc
Listing 4-17.random.txt
所以,这里发生的事情是,在io_monad.php
中,你使用 IO 单子以类似于 Writer 单子的方式设置你的函数链。在第二个文件run_io.php
中,您实际上使用名副其实的unsafePerform()
方法调用了这个函数链。为什么要这样构造呢?第一个文件中的所有函数在实际运行之前都是“纯”的。第一个文件中的函数仅仅构造不纯的函数(通常称为延迟函数);他们实际上并不运行它们,也不做任何 I/O,所以它们就像积雪一样纯净。这意味着io_monad.php
,就其本身而言,是一个纯粹的函数式程序,可以进行全面的测试、推理等等。如果这看起来有点像欺骗,那是因为它是。测试的价值可能是有限的,因为它可能不会测试你的程序的“肉”,其中大部分功能依赖于不纯的动作。当然,您并不局限于以这种方式使用不纯函数;你可以巧妙地将它们与纯调用和其他单子混合在一起,所以当你的程序更多的是纯的而不是不纯的时候,像这样构造它的可测试性就会增加。
了解更多关于单子的信息
如果前几节已经让你对单子感到兴奋,那么接下来的“进一步阅读”部分将帮助你了解单子背后的数学细节和理论。警告:有时阅读量会很大,所以先给自己冲杯咖啡。如果你对单子不感兴趣,那么你会对第七章感兴趣,这一章着眼于结构化应用(剧透:我建议完全可以忽略单子及其同类)。
进一步阅读
- 单子的维基百科条目,给出了一般背景
- [
en.wikipedia.org/wiki/Monad_(functional_programming)
](https://en.wikipedia.org/wiki/Monad_(functional_programming)
- [
- 维基百科中关于幺半群的条目,再次给出了一般背景
- 单子和生殖腺道格拉斯·克洛克福特的谷歌技术演讲视频,他用 JavaScript 解释单子,以一种合理的可理解的方式
- 不要害怕单子,布莱恩·贝克曼解释单子的 MSDN 视频,这次用了更多的数学
- Haskell 手册中关于单子的部分(Haskell 负责在编程中推广单子,这是对单子相当简洁的处理。)
- 三个单子公理的(面向 Haskell 的)解释
- 单子的物理类比,来自 Haskell wiki,通过 Wayback 机器
- 相当全面的 monad 教程和相关文章的时间表;虽然它在 Haskell wiki 上,但它不是 Haskell 特有的
蹦床递归
如果你认为蹦床只对孩子有意思,你显然不是一个函数式程序员!在前一章中,你已经看到了递归,它是一种非常有用的程序控制形式,在很多情况下可以用来代替传统的命令式循环。我还提到了一个主要的缺点,那就是(潜在地)无限的资源使用导致了堆栈溢出等等。
在递归中,你创建一个函数,然后这个函数调用它自己。PHP(和大多数编程语言)的工作方式是,对于每个仍然活动的函数,关于该函数的信息保存在调用堆栈中。每次你的函数调用它自己,一个新的函数就被激活;因此,另一帧信息被添加到堆栈中,使用更多的内存来保存程序的当前状态。原始函数仍然是活动的,等待它刚刚调用的自身复制的结果,所以在复制完成之前它不能从堆栈中移除,依此类推。只有当递归函数的最内层调用完成时,堆栈才会展开,然后所有先前的函数也可以完成。如果在此之前用完了分配的内存,就会出现堆栈溢出错误(或者机器崩溃,这取决于对堆栈大小的任何强制限制)。与简单的while
或for
循环相比,这里唯一保持的状态是任何相关变量的当前状态。每个循环可能会改变它们,但(通常)不会增加所保存的状态信息量。
因此,避免递归问题的一个方法是不使用它,重写任何递归函数,这些函数可能会像命令循环一样破坏堆栈。然而,这不是很实用,意味着你错过了编写递归代码的好处。当然,您可以在代码中进行一些硬限制检查,以确保您的递归只在那些您可以保证它将在可用堆栈/内存限制内完成的值上调用,但是这些可能很难预先确定,这意味着一些计算根本无法完成。
许多语言提供了一种叫做尾部调用优化(TCO)的解决方案。这是编译器使用的一个技巧,让它将某些递归函数展平成命令式的循环结构。当递归调用(对自身的实际调用)是函数中的最后一次调用(尾部调用),只调用自身时,会发生这种情况。此时,编译器可以重用包含函数状态的帧,而不是创建额外的帧,因为它知道调用函数没有其他操作要执行。这极大地减少了存储的信息,实际上使递归变成了一种循环。
不幸的是,PHP 虚拟机不使用 TCO。如果是的话,你可以确保你的递归函数是在函数的末尾用递归调用编写的。相反,您可以使用一个 trampoline 函数来完成基本相同的任务。在计算中,trampoline 是一个自动创建另一个函数来帮助调用另一个函数的函数,在这种情况下“弹跳”您的函数以避免递归!
在看蹦床函数之前,有必要全面了解一下什么是尾调用。尾部呼叫具有以下特征:
- 是递归的(即,它必须是调用自身的函数)
- 必须是返回语句(即
return this_function()
) - 必须只返回它自己
- 必须是最后执行的函数,如果它是一个“返回”值
- 调用中必须没有其他操作发生(即不返回
$something+this_function()
)
如果你的功能不满足这些标准,那么它就不能被 TCO'd,你的蹦床就不能工作。有很多方法可以将大多数递归函数重写为尾调用递归函数(TCR ),如果需要的话,可以搜索一下。
对于这个例子,您将使用大多数 TCO 文章使用的经典例子——阶乘函数。如果你不熟悉,一个数 x 的阶乘(通常写成 x!)是 x * (x-1) * (x-2) * … * (1)。换句话说,5!= 5 × 4 × 3 × 2 × 1 = 120.这可以在标准递归函数中实现,并且总是有一个递归尾调用。您将使用 Gilles Crettenand 编写的函数式 PHP 库中的一个蹦床实现,该实现可从 GitHub 的 https://github.com/functional-php/trampoline
获得;可以通过 Composer 安装,也可以直接包含。参见清单 4-18 和清单 4-19 。
<?php
# Include and use the trampoline library
include('trampoline/src/Trampoline.php');
include('trampoline/src/functions.php');
use FunctionalPHP\Trampoline as T;
# First define our standard recursive function
$factorial = function ($i, $total = 1) use (&$factorial) {
# if $i is 1, return the total, otherwise
# recursively call the function on $i-1,
# multiplying the accumulating total by $i
return $i == 1 ? $total : $factorial($i - 1, $i * $total);
# note that $factorial is the tail call here
# when it is returned
};
# Now the same function again, but this time using the
# trampoline function. The only difference (other than
# the name!) is that we wrap the tail call in T\bounce()
$bounced_factorial = function ($i, $total = 1) use (&$bounced_factorial) {
return $i == 1 ? $total : T\bounce($bounced_factorial, $i - 1, $i * $total);
};
# We use T\trampoline() to call the "bounced" function.
# We'll wrap it in a helper function called $trampolined
# for ease of use
$trampolined = function ($i) use ($bounced_factorial) {
return T\trampoline($bounced_factorial, $i);
};
# We'll create a function to time how long our
# function runs take, in seconds
$timer = function($func, $params) {
$start_time = microtime(true);
call_user_func_array($func,$params);
return round(microtime(true) - $start_time,5);
};
# So let's run our normal recursive function
# and the trampolined version, both to
# calculate the factorial of one hundred thousand.
# The result will be the same, we're only
# interested in the time they take here.
var_dump ( $timer($factorial, [100000]) );
var_dump ( $timer($trampolined, [100000]) );
# Now let's limit the memory we're working with
# and run them again, this time to calculate
# the factorial of one million. We'll run the
# trampolined first, for reasons that you will
# see.
ini_set('memory_limit','100M');
var_dump ( $timer($trampolined, [1000000]) );
var_dump ( $timer($factorial, [1000000]) );
Listing 4-18.
bounce.php
float(0.0254)
float(0.07143)
float(0.63219)
PHP Fatal error: Allowed memory size of 104857600 bytes exhausted (tried to allocate 262144 bytes) in bounce.php on line 18
Listing 4-19.bounce-output.txt
如果您看一下输出,您会看到标准递归函数(第一行)的运行速度要比蹦床版本(第二行)快得多。然而,当您开始处理大数字时,尽管践踏版本需要更长的时间来运行(第三行),递归版本(第四行)会耗尽内存并关闭脚本。
因此,使用蹦床的代价是较低的性能(就执行时间而言)。在某些情况下,这可能意味着您坚持使用普通的递归版本。但是要记住,完成速度较慢的函数通常比完成速度较快的函数要好。考虑你的脚本的用户将会有哪些可用的资源,以及你期望你的脚本处理哪些输入,并且在适当的时候使用一个蹦床版本来安全地运行它。
递归 Lambdas
有趣的是,注意我之前写阶乘函数的方式。我使用闭包而不是命名函数,这在像这样的玩具程序中纯粹是出于选择,但是在真实的程序中,您可能有很好的理由这样做,以允许您获得我在本书前面谈到的闭包带来的优势。现在,递归函数(无论是命名函数还是闭包)需要能够调用自身。对于命名函数来说,这很简单;它在全局空间中有一个名字,所以你可以很容易地从它内部调用它。然而,在一个闭包里,这就不那么简单了。因为从技术上来说,它是一个对象,这里你通过把它赋给一个全局变量,在全局范围内声明了它,它不存在于它本身的范围内。您可能认为您只需要“使用”您赋予它的变量,就像您在闭包中使用任何变量一样。如果您尝试这样做,您将得到一个错误,因为您只能“使用”一个存在的变量(因此有一个值,因为默认情况下您在 PHP 中通过值传递)。变量直到闭包创建后才存在,在此期间,错误将会发生,因为变量不存在!正如您将从我的代码中看到的,解决方法是通过引用use
子句来传递它(在变量前放置一个&符号),这回避了还没有值被传入的事实。
这种递归闭包通常被称为递归 lambda (lambda 是匿名函数的另一种说法)或匿名递归。传统的编程智慧(不管是什么)宣称这是不可取的,要在命名函数上实现递归以保持代码清晰易懂。然而,许多语言都支持匿名递归,比如 JavaScript,它提供了反射功能使之变得更容易,正如你所看到的,我喜欢它!一如既往地务实,看看什么最适合你的代码。
PHP 类型系统
您可能知道,PHP 是一种动态类型语言,与静态类型语言相反。它也是弱类型的而不是强类型的。这意味着您不必指定类型(整数、字符串、布尔值等。)声明变量时(在编译时)。该类型是隐式的,由运行时分配给变量的值决定。弱类型意味着您可以通过为其分配不同的值来更改类型。当需要处理变量时,PHP 会自动将变量的类型转换为所需的类型,例如,当您试图添加一个整数和一个字符串时。在 PHP 中使用弱动态类型非常有用,但这也是很多新手(特别是那些来自静态或强类型语言的人)犯的错误,并导致 PHP 的一些负面关注。为什么 PHP 是动态/弱类型的?好吧,回到时间的迷雾中,当拉斯马斯·勒德尔夫创建 PHP 时,它的目的是作为一种简单、直接的方式来创建交互式网站,而不是一种完全成熟的通用编程语言。当时使用弱动态类型似乎是显而易见的;毕竟,它是为处理网站而设计的,对于 HTTP 来说,没有整数和布尔值的概念——一切都是字符串!因此,如果脚本中的所有内容都是强静态类型的,那么要获得任何有用的值作为输入并将任何内容作为输出发送回来,您的脚本将需要进行大量的类型争论。那么,为什么不把所有的麻烦都去掉,让它变得弱而有活力呢?
相比之下,在大多数函数式编程语言中,强静态类型非常流行。以 Haskell 为例;它有一个强大的静态类型系统,尽管这种被推崇为函数式编程典范的语言也用其恰当命名的动态类型向动态语言的实用性致敬。静态类型在函数式编程中占重要地位的主要原因可以追溯到函数式编程带来的一个主要优势:轻松阅读和推理代码的能力。思路是,如果您被迫在代码中显式声明变量的类型(包括函数的参数类型),那么在给定特定函数的定义的情况下,您可以很容易地推断出当您通读函数实现时,任何给定的输入将会发生什么。当然,除了函数式编程之外,静态类型化还有其他很好的理由,比如编译器/解释器能够在程序开始运行之前发现某些类型的错误。
当然,也有不利的一面,并不是每个人(包括我)都认为静态类型在函数式编程中完全是一件好事,尤其是在 PHP 中。正如您马上会看到的,尽管它本质上是动态的,PHP 确实有一个类型声明系统,以前称为类型提示,可用于建议或强制函数参数和返回类型的特定类型。但是,使用这种语法工具只能带来有限的好处。它不是强类型,所以当变量作为参数传入时,虽然您(和编译器)可以推断变量的类型,但是一旦您在函数中对变量做了任何事情,任何显式类型保证都将失效。并且假设您从一个打开严格类型检查的文件中调用该函数;否则,它只不过是“指导”,在任何阶段都不提供任何保证,只是试图将一种类型扯到另一种类型。尽管好处不多,但声明每种类型的额外语法降低了代码的简洁性和可读性。但是没有输入信息,你怎么能对你的代码进行推理呢?我建议你用同样的方式来思考这个问题,即使你使用了类型声明。如前所述,函数的内部(因此,代码库的很大一部分)将不会考虑您所做的类型声明。以清单 4-20 (和清单 4-21 为例。
<?php
declare(strict_types=1);
function my_function(bool $a) {
var_dump($a);
$a = $a * 22;
var_dump($a);
};
my_function(true);
Listing 4-20.types1.php
bool(true)
int(22)
Listing 4-21.types1-output.txt
如您所见,您已经用declare
语句打开了严格类型,并且使用了类型声明来声明$a
是一个布尔值。在第一个var_dump
中一切都是好的,但是等等,第二个var_dump
告诉你$a
是一个整数。“当然,”你说,“当你把一个布尔值乘以一个数时,PHP 把这个布尔值转换成一个int
,true
变成 1,所以 1 * 22 是 22,这是一个int
,所以这是意料之中的。”很好。我刚刚展示了在使用类型声明时如何对代码进行推理,这是通过理解 PHP 使用的动态类型系统来实现的。您可以使用这些知识以同样的方式推理非类型化函数。事实上,你不一定知道你开始的类型,这并不会让你远远落后于你知道的情况。事实上,如果您的函数可能被不使用严格类型的其他人使用,那么假设您的类型声明会受到尊重而不是防御性地编码可能是危险的。PHP 只对用declare(strict_types=1)
语句从文件中调用的函数执行严格的类型检查,不管定义该函数的文件是否有该声明。因此,如果您的函数假设它将接收一个整数,因为您已经将int
指定为类型声明,并在您的文件中打开了严格类型,但是其他人包含了不使用严格类型的文件,那么您的函数很可能会改为使用float
来调用。当发生这种情况时,在许多情况下,PHP 可以将一种类型强制转换为另一种类型,不会抛出类型错误。float 会被悄悄地强制成整数值(截断,有人吗?).这种强制是在正常的 PHP 规则下发生的,当它需要争论类型时就会发生,但是您的代码不知道它已经发生了,即使您预料到了,您也不能测试它。如果没有类型声明,您的函数将获得 float,因此您可以测试它,并选择当您想要的是整数时如何处理它。
另外,纯函数式编程语言(和/或程序员)经常在代码中避免赋值,部分是为了确保不变性。例如,在这种情况下,不为变量赋值将保证类型保持不变。但是,没有分配导致疯狂,所以你不会在这里练习那种黑暗的艺术。
在使用类型声明进行函数式编程时,另一个问题(尽管更具理论性)是 PHP 只允许在函数之外捕捉和处理类型错误(例如,通过将函数调用封装在try
/ catch
块中),这意味着这种错误实际上是副作用。如果你回头看看第二章中关于副作用的讨论,你会发现如果你在函数本身内部捕捉并处理一个错误,那么就没有副作用。最好的方法是在函数中处理参数和任何必要的测试/转换,这意味着不要使用类型声明。
正如您所猜测的,许多开发人员和专家认为,即使是 PHP 的有限类型强制也是利大于弊的,归根结底,决定您是否在特定程序或函数中使用类型声明的通常是个人选择或实用考虑。在本书的其余部分中,您不会使用它们来保持代码的整洁并专注于其他主题,但是在这里,如果您想要或需要,我将概述如何使用 PHP 类型系统。
类型声明
在 PHP 5 中,类型声明被称为类型提示。5 和 7 之间的主要区别(除了名称更改)如下:
- 在 5 中,违反类型提示会导致可恢复的致命错误;在 7 中,不符合类型声明会导致类型错误。
- 在 5 中,提示支持的类型只有
class
/interface
、self
、array
和callable
。7 中的声明添加了标量(bool
、float
、int
、string
)类型。 - 仅在 7 中支持严格类型。
- 仅在 7 中支持返回类型。
注意,根本不支持类型别名,所以例如,您不能使用boolean
来指定bool
类型。PHP 将别名视为类名,所以它会假设类型为boolean
的参数期待来自boolean
类的对象,而不是bool
(真/假)标量变量。
您也可以将参数设置为“可空”也就是说,它将接受空值以及指定类型的值。为此,通过在参数名称后添加=null
将参数的默认值设置为 null。
除了为每个参数指定类型之外,还可以为返回值指定类型。参数类型在每个参数之前,返回类型在参数列表之后用冒号和 type 指定。
打开严格类型(在 PHP 7 中可用)后,与指定类型不匹配的参数或返回值将导致类型错误。关闭严格类型(这是默认设置),在 PHP 的正常规则下,参数和返回值将被强制转换为指定的类型,而不会抛出错误。如果强制是不可能的(例如,从字符串“hello”到整数),则抛出类型错误。
您可以在同一个函数中混合和匹配带有和不带声明类型的参数。同样,指定返回类型是可选的,不依赖于指定参数类型。
让我们看看一些示例代码中的这些要点(参见清单 4-22 ,清单 4-23 ,清单 4-24 ,清单 4-25 )。
<?php
# Examples of non-strict typing
# Our function accepts two nullable ints, and returns an int
$add = function (int $a = null, int $b = null) : int {
return $a + $b;
};
var_dump( $add(7, 3) ); #10
var_dump( $add(2.5, 4.9) ); #6, not 7.4
var_dump( $add("5Three", "6Four") ); #11, plus Notices thrown
var_dump( $add(true, false) ); #1 (true == 1, false == 0)
var_dump( $add(null, null) ); # 0 (null is coerced to 0)
var_dump( $add("Three", "Four") ); # Type Error
Listing 4-22.types2.php
int(10)
int(6)
PHP Notice: A non well formed numeric value encountered in types2.php on line 7
PHP Notice: A non well formed numeric value encountered in types2.php on line 7
int(11)
int(1)
int(0)
PHP Fatal error: Uncaught TypeError: Argument 1 passed to {closure}() must be of the type integer, string given, called in types2.php on line 23 and defined in types2.php:7
Stack trace:
#0 types2.php(23): {closure}('Three', 'Four')
#1 {main}
thrown in types2.php on line 7
Listing 4-23.types2-output.txt
<?php
# Turn on strict typing
declare(strict_types=1);
# A function which accepts $a of any type,
# and a nullable int $b, and return a
# value of type int
$divide = function ($a, int $b = null) : int {
if ( ($a / $b) == intdiv($a, $b) ) {
return intdiv($a, $b); # returns an integer
} else {
return $a / $b; # returns a float (not good!)
}
};
# As we'll be experiencing a lot of errors, lets create
# a function to catch and deal with the errors so the
# script can complete all of our calls without dying
function run($func, $args) {
try {
# run the function and var_dump the return result
var_dump( call_user_func_array($func, $args) );
} catch ( Error $e ) {
# print the error message if one occurs
echo "Caught : ".$e->getMessage()."\n";
}
};
run( $divide, [10, 2] ); # int(5)
run( $divide, ["10","2"]); # Type Error, as no type coercion
run( $divide, [10, 2.5] ); # Type Error, as no type coercion
run( $divide, [true, false] ); # Type Error, as no type coercion
run( $divide, [23, null] ); # Division by zero warning & intdiv type error.
# Note that our input parameter is declared an int, and intdiv requires
# an int. But we still get an error, because ints are nullable in
# user function parameters, but not in all PHP function parameters
run( $divide, [10,3]); # Return Type Error (float 3.3333333...)
run( $divide, [6.4444 % 4.333, 9.6666 % 2.0003]); # int(2)
# all that matters is the type of the value of an expression passed
# as a parameter, not the types of the operands of that expression.
Listing 4-24.types3.php
int(5)
Caught : Argument 2 passed to {closure}() must be of the type integer, string given, called in types3.php on line 36
Caught : Argument 2 passed to {closure}() must be of the type integer, float given, called in types3.php on line 36
Caught : Argument 2 passed to {closure}() must be of the type integer, boolean given, called in types3.php on line 36
PHP Warning: Division by zero in types3.php on line 13
Caught : intdiv() expects parameter 2 to be integer, null given
Caught : Return value of {closure}() must be of the type integer, float returned
int(2)
Listing 4-25.types3-output.txt
因此,正如你所看到的,如果你使用类型声明,最好确保你知道所有关于它们如何操作的注意事项,不要让自己陷入虚假的安全感。例如,如果您打开了严格类型,声明函数的返回类型为float
,并尝试返回一个int
,您认为会发生什么?还记得我说过 PHP 不会试图强制类型吗?所以,你应该得到一个返回类型错误,对不对?让我们试试(见清单 4-26 和清单 4-27 )。
<?php
# Turn on strict typing
declare(strict_types=1);
# Declare two functions that are EXACTLY
# the same apart from the return type (and name).
# intdiv returns an integer. (int) casting
# ensures that even if we've somehow messed
# up, intdiv returns an int into $a, and
# the return value is forced to int.
$the_func_int = function () : int {
$a = (int)intdiv(10,2);
return (int)$a;
};
$the_func_float = function () : float {
$a = (int)intdiv(10,2);
return (int)$a;
};
var_dump( $the_func_int() ); # int(5). As expected.
var_dump( $the_func_float() ); # float(5). Errr?!
Listing 4-26.types4.php
int(5)
float(5)
Listing 4-27.types4-output.txt
没有错误,当它离开你的手的时候,返回一个肯定是int
的东西,从另一端出来就是一个 float!考虑到这一点,这是有意义的,因为 PHP 中任何有效的int
都可以表示为一个有效的 float。但是假设严格的类型化坚持使用完全相同的类型名,例如,var_dump
,就会带来麻烦。你可以在 PHP 手册中了解更多关于类型声明的细节。
进一步阅读
- PHP 手册中的类型声明
- PHP 手册中的返回类型声明
作为一个有点轻率的事后想法,认为这个世界上没有什么新的东西。在 Perl、PHP 和 Python 这样的新贵带着他们时髦的现代弱动态系统出现之前,强静态类型是多年前类型管理的主要形式,这是未来的发展方向。读一读最近的 PHP“最佳实践”网站,或者听听伟大的 PHP 改革家们讲述他们希望如何发展这门语言,你很快就会发现,对于 PHP 来说,要真正完成向现代范式的转变,它需要有适当的强大的静态类型……于是轮子又转了。
摘要
在这一章中,你看了几个更高级的函数式编程主题。当您得知许多纯函数式语言的程序员将这些视为一般的主题时,您不会感到惊讶,事实上,您可能会发现它们在解决许多函数式“问题”时非常有用鉴于一些函数式语言缺乏命令性和“不纯”的功能,它们是完成任何实际工作所必需的。然而,由于 PHP 提供了将函数式代码与命令式代码混合和匹配的灵活性,并且假设您不是执意只编写函数式代码,那么您通常可以在不使用这些技术的情况下编写函数式代码。用你作为程序员的判断和经验来决定什么时候它们会增强你的程序,什么时候你只是为了“功能性”而写这样的代码。
五、高性能应用的策略
在本书的第一部分,我介绍了函数式编程背后的理论,概述了如何用函数式风格编写代码。我提到了函数式编程的一些好处,但在第二部分中,我将通过创建一些程序来展示函数式编程可以简化的一些实际任务,从而使这些好处加倍。
本章着眼于使用函数式编程来提高脚本的性能。在过去,PHP 在性能部门受到了不好的评价,这并不完全令人惊讶,因为它本质上是一种高级解释语言。然而,在过去的几个主要版本中,性能有了很大的提高,尤其是版本 7,它的性能远远超过了 PHP 以前的任何版本。所以,在你开始学习本章的技术之前,确保你运行的是最新版本的 PHP,因为这可能是提高性能最简单的方法。也就是说,即使您已经升级到了最新最好的版本,也经常会有这样的时候,您可以对有问题的脚本进行额外的性能提升。您将看到函数式编程技术,如记忆化和惰性求值,它们有助于提高脚本速度,以及函数式编程如何帮助您利用并行编程来提高性能。但是在开始之前,理解我所说的性能是什么以及如何衡量它是很重要的。
了解和衡量绩效
当您的脚本运行时,它将使用一定数量的内存和 CPU 和/或墙时间。其中任何一项的降低通常都被认为是性能的提高。有时,其他资源,如磁盘空间、网络带宽或 API 调用,也被认为是性能问题。您的特定性能要求将决定在什么情况下您认为性能得到了提高。例如,在具有大量磁盘空间但处理器有限的 NAS 机器上,您可能认为以增加缓存磁盘空间为代价来优化较低的内存和 CPU 使用是理想的。但是,如果您的目标是配备小型固态硬盘的高性能笔记本电脑,您可能会考虑使用内存,因为缓存存储是一种更好的权衡。在这一章中,你将主要关注内存使用和墙时间,这是 PHP 脚本经常遇到的两个最大的性能问题。
衡量绩效:剖析
您可能遇到过运行缓慢的脚本(通常是您自己的脚本!),通常第一反应是开始寻找提高 PHP 速度的方法。编译、缓存、重构代码、加速器——这些都是在搜索 PHP 性能或速度问题时很容易出现的话题。你可能已经读到过它们,并想尝试一下。
我的建议(来自痛苦的个人经历)是立即停止。对您的代码抛出一个又一个性能技巧(通常您可以在网上或类似的好书中找到),即使它们看起来很合理,并且您可以看到其中的逻辑,也可能会使您的代码变得复杂,或者无缘无故地增加依赖性。为什么呢?因为当你不知道问题的根本原因时,你就不知道一个特定的解决方案,不管在理论上有多好,是否能解决你在特定情况下遇到的问题。即使它看起来确实有效,你也不知道这是否是解决它的最简单的方法,因此当你不需要的时候,你是否会让自己背上额外的“技术债务”。
你经常错过的一步是直接问你的剧本“你怎么跑这么慢?”如果您的脚本告诉您,那么您可以尝试在不使用外部工具(如编译器和缓存系统)的情况下修复这个问题。那么,你如何向你的剧本提出为什么的问题呢?通过侧写。
分析器在软件运行时监视它(通常从“内部”),并分解程序每个部分所用的时间(有时是资源)。概要文件信息通常被报告到单独的代码行或函数调用的级别。这有助于您准确定位脚本变慢的地方。是因为复杂数据库查询吗?写得不好的循环?一个被调用次数超过预期的函数?磁盘或网络访问暂停执行?无论是什么问题,侧写员都会告诉你。一旦你知道了减速的确切原因,解决方案通常是显而易见的(或者至少,你可以排除实际上不会解决问题的潜在解决方案)。这可能只是意味着重写几行代码或缓存一些数据,而不是重复生成数据。剖析器可能会指出 PHP 外部的问题,比如缓慢的数据库服务器或落后的网络连接或资源。当然,在某些情况下,从 PHP 编程的角度来看,您可能最终会遇到一个棘手的问题,这确实需要加速器或外部缓存系统的帮助。在任何情况下,通过在开始尝试什么之前使用一个分析器来询问为什么,您可能会节省时间并防止对您的代码或部署环境进行不必要的更改。
对于 PHP,在进行概要分析时,您有几种选择。您可以通过将分析/测量语句直接添加到您的代码库中来手动分析您的代码,或者您可以使用一个工具来自动分析您的代码。如果您大致知道问题出在代码的什么地方,那么前者简单快捷,不需要改变您的开发环境。后者虽然需要安装和配置该工具,并在第一次学习如何使用它,但它提供了更全面的分析。它也不依赖于您知道您的问题可能在哪里,并且通常只需要对您的代码库进行最小的修改或者不需要修改。在接下来的小节中,您将会看到这两个选项。
手动剖析
手动分析需要在源代码中添加代码,以便直接从脚本中测量时间或资源。清单 5-1 展示了一个测量不同代码行执行时间的例子,输出如清单 5-2 所示。
<?php
# A script to do some "busywork", filling
# some strings with some characters.
# Let's create a "checkpoint" by recording the current time and memory
# usage
$time1 = microtime(true);
$memory1 = memory_get_usage();
# Now let's do a loop 10 times, having a quick usleep and
# adding just a little data to our variable each time
$a_string = (function () {
$output = '';
for ($counter = 0; $counter < 10; $counter++) {
usleep(10);
$output .= 'a';
};
return $output;
})(); //we execute the function straight away
# Now create a second checkpoint
$memory2 = memory_get_usage();
$time2 = microtime(true);
# Let's do this second loop 1000 times, having a longer
# sleep and adding lots of data to our variable each time
$b_string = (function () {
$output = '';
for ($counter = 0; $counter < 10; $counter++) {
usleep(100);
$output .= str_repeat('abc',1000);
};
return $output;
})(); //again we execute straightaway
# and create a final checkpoint
$memory3 = memory_get_usage();
$time3 = microtime(true);
# Now let's output the time and memory used after each function.
echo "1st function : ".($time2-$time1)." secs, ".
($memory2-$memory1)." bytes\n";
echo "2nd function : ".($time3-$time2)." secs, ".
($memory3-$memory2)." bytes\n";
echo ("Peak memory usage : ". memory_get_peak_usage()." bytes\n");
Listing 5-1.manual.php
1st function : 0.0007178783416748 secs, 40 bytes
2nd function : 0.0016269683837891 secs, 32768 bytes
Peak memory usage : 392504 bytes
Listing 5-2.manual-output.txt
如您所见,第二个函数比第一个函数花费的时间长得多。你现在知道了使你的脚本变慢的问题是第二个循环,你可以通过删除usleep
语句或者删除整个循环并用str_repeat('abc',1000000)
填充你的字符串来修复它。
在查看每个函数调用前后使用的内存量时,您需要稍微谨慎一些。如您所见,第二个函数后内存使用的差异比第一个函数大得多,这是意料之中的,因为您已经返回了一个大字符串。但是,该脚本的内存使用峰值高于两个单独测量值的总和。函数在运行时会使用内存,但是一旦它们返回,内存通常会被释放(静态变量和生成器不会被释放),只留下返回值所占用的内存(假设您选择了捕获它)。即使您在您的return
语句之前添加了一个memory_get_usage()
调用,如果您在函数执行时销毁或替换变量值,它也可能不会捕获函数使用的所有内存。在进行过程中,您需要仔细考虑您的脚本正在做什么,以及放置手动分析语句的最佳位置。
这显然是一个简单的、人为的例子,但是这些原则也适用于真实世界的代码。正如您所看到的,对于这里或那里的几行代码或一个函数,手动分析是快速而简单的。然而,分析较大的代码库很快会变得很麻烦,如果不小心的话,会大大增加代码库的大小。在寻找一个特定的问题时,您可以分析代码的较大部分,当发现有问题的较大部分时,您可以将它分析成较小的部分,等等,直到找到问题代码(有效地进行二分搜索法)。如果您花费大量的时间来这样做,那么实现和学习像下面详述的那些剖析工具所必需的时间可能会是值得的。
另一件要记住的事情是,手动分析代码会增加脚本的性能损失——虽然通常很小,但累积起来会很大,特别是在您不断重复地将分析信息记录到磁盘的情况下。因此,在代码投入生产之前(可能作为构建/部署过程的一部分),考虑剥离或禁用概要分析代码是值得的。当然,在某些情况下,有意识地将概要分析代码添加到产品代码库可能会有所帮助(例如,当从您的终端用户那里收集概要分析信息是必要的/有用的时候,这些用户可能没有安装专用的概要分析软件)。自动分析工具通常也会增加一些开销,尽管它通常更小(它们通常是用低级语言编写的,并且通常直接与 PHP 解释器集成),并且通常更容易打开和关闭。这些自动化工具通常只在开发环境中使用,而不在实际生产机器上使用,因此任何开销都仅限于开发工作。
分析工具
PHP 有几个可用的分析工具。虽然流行的 Xdebug 调试器提供了一些分析选项(如果您已经安装了用于调试的工具,那么值得一看),但是最常见和最全面的工具是 XHProf。最初由脸书开发,它是 PECL 的扩展,因此可以简单方便地安装。数据收集端是用 C 编写的,提供了一个图形化的 PHP 接口,用于查看收集到的概要数据,包括调用图(哪些函数调用了哪些函数的可视化图形),如果你安装了 Graphviz 的话。两个相关的项目 XHProf UI 和 XHGui 提供了一个扩展的可视化界面,在一个 MySQL 或 MongoDB 中存储多个运行,并提供对多个运行进行排序和比较的访问。这些需要安装和配置的工作要多一点,但是如果您经常在生产系统上分析开发代码或实时代码,它们会提供很大的灵活性。不过,对于在开发代码中发现明显问题的基本剖析,XHProf 本身是一个很好的起点。
领先的商业 PHP 分析工具是 Blackfire,它提供了相当全面的分析服务,并且在撰写本文时有一个合理的免费层。请注意,虽然分析客户端在您的系统上运行,但数据会报告给 Blackfire 服务器后端,因此可能不适合某些用途。
进一步阅读和工具
- “对速度的需求:用 XHProf 和 XHGui 剖析 PHP ”,作者 Matt Turland
- Adam Culp 的“XHProf PHP 剖析”
- Lorna Mitchell 的“用 XHGui 分析 PHP 应用”
XHProf 老师
函数级分层 PHP 分析器
- 主网站:
https://github.com/phacility/xhprof
- 安装信息:
www.php.net/manual/en/xhprof.setup.php
- 主要文档:
www.php.net/xhprof
- 可视化函数图形工具:
http://graphviz.org/
xh 教师 UI
基于 XHProf 的扩展分析器
XHGUI
基于 MongoDB 的 XHProf 数据图形界面
Xdebug
内置分析器的综合调试器
- 主网站和文档:
https://xdebug.org/
KCachegrind
轮廓数据可视化工具。与 Xdebug 一起使用以获取可视配置文件信息。
Webgrind
Xdebug 的另一个基于 web 的概要分析前端,它实现了 KCachegrind 功能的子集
逆火
商业 PHP 分析服务
低级剖析
当你真的需要“深入”你的脚本时,有时你需要看的不是你的代码在做什么,而是 PHP 本身在做什么。明确地说,我们大多数人永远不需要这样做来解决性能问题,尽管看一看 PHP 如何将您的代码翻译成对运行它的系统的调用是非常有趣和有益的。PHP 本身是一个编译成二进制可执行文件的 C 程序,这意味着您可以使用 strace(显示系统调用和信号)、ltrace(显示库调用)和 gdb(类似 PHP 本身的 C 程序调试器)等通用工具来查看幕后发生的事情。如果你对此感兴趣,可以看看吴镇男·雷森斯的以下教程。作为 Xdebug 的作者,他在某种程度上是 PHP 机制方面的专家。
所以,现在你已经剖析了你的代码,你知道你的瓶颈在哪里,你解决了任何简单的新手错误。您已经运行了最新版本的 PHP,并且您在顶层硬件上。因此,您相当确信您的问题出在代码库的特定部分,并且您需要以更有效的方式编写一些算法。函数式编程能提供一些模式来帮助加速你的代码吗?当然可以;否则,这将是一个非常短的章节!下面几节介绍了一些功能性技术,这些技术适用于许多情况,即使您的整个代码库都不起作用。
进一步阅读
- “PHP 在做什么?”作者吴镇男·雷森斯
记忆化
如果你已经编程一段时间了,特别是在 web 领域,你会遇到缓存的概念。缓存是这样一个过程,即获取“昂贵”计算的结果,存储结果,然后在下次调用该计算时使用存储的结果,而不是再次运行计算本身。昂贵意味着运行时间长,占用大量内存,进行大量外部 API 调用,或者执行任何其他出于成本或性能原因而希望最小化的操作。缓存失效是您选择从缓存中移除项目的过程。例如,如果生成新闻网站的首页需要很大的努力,那么您会希望缓存该页面,这样就不需要在每次访问者访问您的网站时都生成它。然而,一旦下一个突发事件发生,您将希望更新您的首页,并且您不希望您的访问者点击缓存版本并获得旧新闻,因此您将“无效”缓存并重新生成页面。如果您曾经参与编写或使用过缓存系统,您无疑会熟悉下面这句话(或者至少理解它的出处),这句话引自 Phil Karlton:
"在计算机科学中只有两件困难的事情:命名、缓存失效和一个接一个的错误."
您已经看到了递归如何减少一个接一个的错误,而且没有人有希望解决事物命名的问题,那么如何解决缓存失效呢?如果你认为函数编程有锦囊妙计,请举手。很好,金星给你!确实如此,诀窍就是永远不要让缓存失效。问题解决了!
我其实是认真的。函数式编程提供了一种称为记忆化的技术,这种技术植根于纯函数固有的属性。在前面的理论章节中,你看到了纯函数是如何透明引用的。给定一组特定的输入参数,一个纯函数将总是产生相同的返回值,并且(对于该组输入)该函数可以简单地用返回值替换。这听起来应该有点像缓存:对于一组给定的输入(比如,您的新闻故事),您希望用返回值(缓存的输出)替换(运行起来很昂贵的)函数。获取一个纯函数的输出并缓存它的过程是记忆化,是缓存的一个特例。
假设您正在记忆一个昂贵的函数,并将结果缓存到磁盘。每次使用不同的参数运行该函数时,可能会得到不同的结果。您想要消除的是对相同参数多次运行函数的成本,因为每次您的(纯)函数都保证给您相同的结果。因此,您可以缓存结果,例如,创建一个散列来表示所使用的输入参数,并使用它作为文件名来存储该运行的返回值。下次运行该函数时,再次散列输入参数,查看是否存在同名的缓存文件,如果存在,则返回内容(而不是重新运行代价高昂的函数)。
到目前为止,这是典型的缓存。但是,如何避免让缓存失效呢?答案是你没有;记忆能有效地帮你做到这一点。你的功能是纯的,这意味着没有副作用。因此,如果在你虚构的新闻网站上出现了一个新的故事,这个故事的细节将只影响(纯)函数,该函数通过输入参数创建你的首页。例如,您可以将一组标题作为一个参数。突然,参数的哈希值发生了变化,所以 memoized 函数将无法在磁盘上找到具有该哈希值的文件,因此将运行完整的函数,并将新结果缓存到磁盘上以新哈希值命名的文件中。概括地说,由于您唯一的输入是参数,如果没有任何参数发生变化,您必须确定可以使用缓存。但是,如果参数已经改变,那么就没有相应的缓存文件,所以没有必要使它无效。当然,旧的缓存文件仍然会在那里,所以当您不小心发布了一个谎称这本书是垃圾的故事时,您可以立即收回它,函数将返回到使用旧的缓存文件,因为哈希将再次匹配参数。
目前为止,一切顺利。然而,函数式编程并没有因为它的优点而止步,哦,不。如果你正在考虑如何编写你的函数来实现记忆,那就停下来吧。一般来说,你不需要。您可以简单地将您的函数包装在另一个自动记忆它的函数中。这样的包装函数很容易编写,因为你所关心的只是你的纯函数的输入和输出,而不是它在内部做什么。
让我们来看一个记忆的例子。在清单 5-3 中,您将把 pure 函数的结果缓存到磁盘中。为了简洁起见,您将把那些不纯的磁盘函数分离成单独的函数,而不是编造一些 IO 单子,但是如果您愿意,当然也可以这样做。
<?php
# We're going to cache our results on disk, so let's
# define a directory and file prefix
define('CACHE_PREFIX', sys_get_temp_dir().'/memo-cache-');
# This is a helper function to read a cached file
# from disk. I've broken it out as a separate function
# as it is necessarily impure. You can replace it
# with an IO monad or similar in production if you wish
$get_value = function($hash) {
# return null if the file doesn't exist
if (!file_exists(CACHE_PREFIX.$hash)) { return null; }
# read the file into $value
$value = file_get_contents(CACHE_PREFIX.$hash);
# return null if the file exists but couldn't be read
if ($value === false) { return null; }
# return our value if all is good
return $value;
};
# Likewise, this is an impure helper function to write
# the value to a cache file.
$store_value = function($hash, $value) {
if (file_put_contents(CACHE_PREFIX.$hash, $value) === false) {
$value = null;
}
# return the value that was stored, or null if the
# storage failed
return $value;
};
# Finally, this is our actual memoization function.
# It returns a closure which is a "memoized" version
# of the function you call it on, i.e. a version
# of your function which automatically caches return
# values and automatically uses those cached values
# without further coding from you.
# $func is the function (closure or other callable) that
# you want to memoize
$memoize = function($func) use ($get_value, $store_value)
{
# We're returning a memoized function
return function() use ($func, $get_value, $store_value)
{
# Get the parameters you (the end user) call
# your memoized function with
$params = func_get_args();
# Get a unique hash of those parameters, to
# use as our cache's key. We needs to convert
# the params array to a string first, we use
# json_encode rather than serialize here as
# it is a lot faster in most cases
$hash = sha1( json_encode( $params ) );
# Check the cache for any return value that
# has already been cached for that particular
# set of input parameters (as identified by
# its hash)
$value = $get_value($hash);
# If there was no pre-cached version available,
# $value will be null. We check this with the ??
# null coalescing operator, returning either :
# a) the cached $value if it's not null, or
# b) the results of actually calling the user
# function. Note that we wrap the call in the
# $store_value function to cache the results,
# and $store_value passes the value back
# through as its result and so it is also
# returned to the user in this case
return $value ?? $store_value(
$hash, call_user_func_array($func, $params)
);
};
};
Listing 5-3.
memoize.php
首先,memoize 函数通过将输入参数编码成 JSON 来制作输入参数的惟一字符串表示。例如,如果您想知道为什么不简单地使用implode("|", $params)
,请考虑以下两个函数调用:
func("Hello","|There");
func("Hello|","There");
这将导致两者都被编码为Hello||There
,因此当它们实际上不同时,被视为相同的参数集。如果你能保证这个字符不会出现在你的参数中,你可以使用带有粘合字符的implode
,但是为了以防万一,通常编写防御性代码并使用适当的序列化函数是个好主意。您可以使用 PHP 的serialize()
函数来代替json_encode
,因为对于某些工作负载,它可能会更快。两者都有边缘情况,你可能想在选择之前熟悉一下,比如serialize()
不能处理某些类型的对象。关于这两者的更多信息,请参见 PHP 手册。
一旦有了输入的字符串表示,就需要将它转换成适合用作文件名的另一个字符串。您的 JSON 字符串可能包含对文件名无效的字符,因此您将为它创建一个 SHA1 散列。MD5 散列的创建速度稍快,但发生散列冲突的可能性更大(为两个不同的输入生成相同的散列)。即使是 SHA1 也会发生碰撞,尽管风险通常很低。如果您肯定无法处理冲突,那么您将需要编写一些代码来解析序列化的字符串,并以一致的方式替换无效字符,等等,确保您保持在缓存介质的其他限制内(例如,写入磁盘的文件名长度)。
现在您有了自己的散列(或者其他描述输入参数的独特方式)。然后,您尝试从缓存中加载一个以 hash 作为名称的文件的内容。如果您无法读取它(通常是因为它不存在,因为这是您第一次使用这些参数进行调用),您可以使用call_user_func_array()
运行 pure 函数,获取它的返回值并创建缓存文件,最后将获取的值作为返回值返回。如果您可以读取该文件,您只需将内容作为返回值返回,并跳过函数的执行。您会注意到这里没有使用任何形式的严格类型。如果你的 pure 函数的返回值是一个int
(比方说),当你第一次运行 pure 函数时,你将把它写到磁盘,并把int
返回给调用者。但是,在随后的运行中,您将缓存文件的内容作为一个字符串获取并返回,因此您的返回值是一个字符串。如果输入在应用中很重要,您可以将值序列化到磁盘中,并在读回时再次将其取消序列化。
现在让我们看一个例子,看看如何实际使用这个 memoize 函数。您将使用另一个经典的示例任务,一个生成斐波那契数列的算法。我使用它是因为它是一个简单易懂的函数,而且是递归的。记忆化对任何函数都有效,不管是递归的还是非递归的,但它通常特别有用,因为递归函数经常会占用大量资源,正如您前面看到的那样。如果你不熟悉斐波那契数列,它是一个数列,其中前两个(或者三个,如果你从零开始)后面的每个数字都是前面两个数字的和,所以:
0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946 等等…
该算法取一个整数 n,并计算序列中的第 n 个数。因此,$fibonacci(7)
将返回 13 (13 是前面序列中的第 7 个数字,从 0 开始)。
您将创建两个函数:一个标准版本的函数和一个包装在早期的$memoize
函数中的函数。通常你只需要创建一个函数并把它包装在$memoize
中。然而,因为我想演示一个递归版本,它递归地调用有记忆的版本(并与无记忆的形式进行对比),所以您将在这里创建两个。由于斐波那契对于现代电脑来说并不是一项特别繁重的任务,你会以usleep
语句的形式添加一些人为的“费用”来使每次计算花费更长的时间。这将展示记忆化对真正长期运行的函数的影响。参见清单 5-4 和清单 5-5 。
<?php
# Get our memoize function and helpers
require('memoize.php');
# Define a plain old recursive fibonacci function
$fibonacci =
function ($n) use (&$fibonacci) {
usleep(100000); # make this time-expensive!
return ($n < 2) ? $n : $fibonacci($n - 1) + $fibonacci($n - 2);
};
# Define the same fibonacci function again in exactly the
# same way (except for the name), but this time wrap the
# function body in a call to $memoize to get a memoized version
$memo_fibonacci = $memoize(
function ($n) use (&$memo_fibonacci) {
usleep(100000);
return ($n < 2) ? $n : $memo_fibonacci($n - 1) + $memo_fibonacci($n - 2);
}
);
# Let's define a timer function, to time a run of a function,
# and return the parameters, results and timings.
$timer = function($func, $params) {
$start_time = microtime(true);
$results = call_user_func_array($func, $params);
$time_taken = round(microtime(true) - $start_time, 2);
return [ "Param" => implode($params),
"Result" => $results,
"Time" => $time_taken ];
};
# And now let's do a set of runs of both our
# ordinary function and it's memoized sister.
# I've added an extra * parameter to the
# non-memoized runs so that you can spot them
# easier in the output (the '*' isn't used
# by the fibonacci functions, it's just passed
# through to the output of the timer function)
print_r( $timer( $fibonacci, [6, '*'] ) );
print_r( $timer( $memo_fibonacci, [6] ) );
print_r( $timer( $fibonacci, [6, '*'] ) );
print_r( $timer( $memo_fibonacci, [6] ) );
print_r( $timer( $memo_fibonacci, [10] ) );
print_r( $timer( $memo_fibonacci, [11] ) );
print_r( $timer( $memo_fibonacci, [8] ) );
Listing 5-4.
memo_example.php
Array
(
[Param] => 6*
[Result] => 8
[Time] => 2.5
)
Array
(
[Param] => 6
[Result] => 8
[Time] => 0.7
)
Array
(
[Param] => 6*
[Result] => 8
[Time] => 2.5
)
Array
(
[Param] => 6
[Result] => 8
[Time] => 0
)
Array
(
[Param] => 10
[Result] => 55
[Time] => 0.4
)
Array
(
[Param] => 11
[Result] => 89
[Time] => 0.1
)
Array
(
[Param] => 8
[Result] => 21
[Time] => 0
)
Listing 5-5.memo_example-output.txt
如果您查看清单 5-5 中第一次运行的输出,您会发现标准函数计算第 6 个斐波那契数需要 2.5 秒,而记忆化版本只需要 0.7 秒。当然,它们在第一次运行时应该是相同的,因为还没有任何东西被缓存。因为你的函数是递归的,你实际上在每次计算中多次调用这个函数,当你的内存化版本多次调用相同的参数时,你的缓存将被使用。
第三次运行演示了用参数 6 再次调用标准函数仍然需要 2.5 秒,这是显而易见的,因为它没有缓存。但是,在 6 上调用 memoized 版本需要 0 秒(四舍五入!)因为在计算中每次递归调用都会命中缓存。
接下来计算第 10 个数,你只需要 0.4 秒。这比计算第 6 个数字要快,因为它们共享一些步骤(每个步骤都需要计算第 1、第 2、第 3 等数字),这些步骤已经被缓存,第 10 个数字只需要实际计算第 7、第 8、第 9 和最后第 10 个数字。下一次运行进一步证明了这一点;计算第 11 个数字现在只需要 0.1 秒(因为它只有一个对函数的未缓存调用),计算第 8 个数字的最后一次运行在 0 秒内,因为它已经在您生成第 10 个数字时的缓存中。
如果您第二次调用该脚本,您会发现所有使用 memoized 函数的运行都在 0 秒内完成,因为您的缓存已经为所有需要的值准备好了,因为您之前至少生成过一次这些值。除非有人改变数学的基本原理,否则您可以永久保持缓存不变,因为对于给定的输入,缓存的结果总是正确的。如果您想知道缓存是什么样子,运行更多的/tmp/memo-cache-*
会给出清单 5-6 中的输出。如您所见,有 12 个文件,这是有意义的,因为您计算了第 11 个斐波那契数(从 0 开始计数),因此调用了具有 12 个不同参数的 memoized 函数。
::::::::::::::
/tmp/memo-cache-10ae24979c5028fa873651bca338152dc0484245
5
::::::::::::::
/tmp/memo-cache-1184f5b8d4b6dd08709cf1513f26744167065e0d
0
::::::::::::::
/tmp/memo-cache-1fb0856518ee0490ff78e43d1b6dae12ad6ec686
21
::::::::::::::
/tmp/memo-cache-2499831338ca5dc8c44f3d063e076799bea9bdff
1
::::::::::::::
/tmp/memo-cache-3ad009a144b1e8e065a75ca775c76b2fc2e5ff76
89
::::::::::::::
/tmp/memo-cache-4a0a63ce33cc030f270c607ea7bf90a6717572bb
8
::::::::::::::
/tmp/memo-cache-7a60554107407bfe358bedce2bfcb95c90a8ea0d
34
::::::::::::::
/tmp/memo-cache-8f4e345e7cd51e4e633816f5a52a47df465da189
3
::::::::::::::
/tmp/memo-cache-bd703dc0b11593277a5a82dd893f2880b8d0f32a
13
::::::::::::::
/tmp/memo-cache-e9310b0c165be166c43d717718981dd6c9379fbe
55
::::::::::::::
/tmp/memo-cache-f1e31df9806ce94c5bdbbfff9608324930f4d3f1
2
::::::::::::::
/tmp/memo-cache-f629ae44b7b3dcfed444d363e626edf411ec69a8
1
Listing 5-6.
cache_files.txt
在这些例子中,您缓存到磁盘,这允许您创建一个持久的缓存,它可以在重新启动后继续存在,并由多个进程使用。但是,有时磁盘太慢,如果您的函数参数经常变化,您可能只想在单个脚本运行期间进行缓存。另一种方法是在内存中进行缓存,事实上 PHP 提供了一种创建变量的方法,这些变量的行为类似于全局变量,但仅限于给定的函数,非常适合在一次脚本运行中进行缓存。这些被称为静态变量,如果你不熟悉它们,清单 5-7 (和清单 5-8 )是静态变量($sta
)的一个例子,与全局变量($glo
)、参数变量($par
)和普通函数范围变量($nor
)相比。
<?php
$my_func = function ($par) {
static $sta;
global $glo;
var_dump( "static : ". $sta += 1 );
var_dump( "global : ". $glo += 1 );
var_dump( "param : ". $par += 1 );
var_dump( "normal : ". $nor += 1 );
return $sta;
};
while ( $my_func(1) < 5) { echo "-----\n"; };
echo "*****\n";
var_dump( "static : ". $sta );
var_dump( "global : ". $glo );
var_dump( "param : ". $par );
var_dump( "normal : ". $nor );
Listing 5-7.
static.php
string(10) "static : 1"
string(10) "global : 1"
string(10) "param : 2"
string(10) "normal : 1"
-----
string(10) "static : 2"
string(10) "global : 2"
string(10) "param : 2"
string(10) "normal : 1"
-----
string(10) "static : 3"
string(10) "global : 3"
string(10) "param : 2"
string(10) "normal : 1"
-----
string(10) "static : 4"
string(10) "global : 4"
string(10) "param : 2"
string(10) "normal : 1"
-----
string(10) "static : 5"
string(10) "global : 5"
string(10) "param : 2"
string(10) "normal : 1"
*****
string(9) "static : "
string(10) "global : 5"
string(9) "param : "
string(9) "normal : "
Listing 5-8.static-output.txt
如你所见,即使你每次都用相同的参数(1)调用my_func
,但是$sta
的值每次都不一样。因此,虽然您不能从函数之外的任何作用域访问它,但它通常仍被视为“副作用”,因为对于函数的任何特定调用,您都无法确定它将处于什么状态(在这种情况下,不知道函数已经被调用了多少次)。那么,如何在函数式程序中使用静态变量呢?答案是,小心翼翼。让我们来看一个例子(参见清单 5-9 )。您将创建一个版本的 memoize 函数,它使用一个静态数组来保存您的缓存,而不是写入磁盘。
<?php
$memoize = function($func)
{
return function() use ($func)
{
static $cache;
$params = func_get_args();
$hash = sha1( json_encode( $params ) );
$cache["$hash"] = $cache["$hash"] ??
call_user_func_array($func, $params);
return $cache["$hash"];
};
};
Listing 5-9.
memoize-mem.php
所以,你放入$cache
数组的所有内容,以及后来从中读取的内容,都完全由你调用函数所用的参数(通过散列)决定,而你放入其中的是那个函数的值。您对静态变量的使用实际上是透明的,所以在这种情况下,您不会产生任何潜在的副作用。如果您像以前一样调用相同的memoize-example.php
脚本,但是使用这个基于内存的 memoize 函数,您将得到清单 5-10 中的输出。
Array
(
[Param] => 6*
[Result] => 8
[Time] => 2.51
)
Array
(
[Param] => 6
[Result] => 8
[Time] => 0.7
)
Array
(
[Param] => 6*
[Result] => 8
[Time] => 2.51
)
Array
(
[Param] => 6
[Result] => 8
[Time] => 0
)
Array
(
[Param] => 10
[Result] => 55
[Time] => 0.4
)
Array
(
[Param] => 11
[Result] => 89
[Time] => 0.1
)
Array
(
[Param] => 8
[Result] => 21
[Time] => 0
)
Listing 5-10.memo_mem_example-output.txt
如您所见,这与基于文件的示例的输出完全相同。它实际上运行得稍微快一点,因为你不做磁盘 I/O,但你在这里四舍五入到最近的 0.1 秒。与基于磁盘的示例相比,另一个唯一的区别是,如果您第二次运行该脚本,您将再次得到这个输出(而不是对于内存化的调用都是零),因为用于缓存的静态变量在脚本结束时被销毁。
除了磁盘和基于会话的内存缓存之外,还有一种替代方案,即简单的 RAM 磁盘。在 Linux 类型的系统上,有一个名为 tmpfs 的文件系统,它允许您创建和使用存储在内存中而不是磁盘上的文件。这些虚拟文件的行为和操作就像磁盘上的普通文件一样,因此可以允许不同的 PHP 进程读写文件中的缓存数据,就像处理普通的“磁盘上”文件一样。tmpfs 带来的优势是双重的;一是快,二是一切都是暂时的。因为文件保存在内存中,没有机械硬盘等待,所以 I/O 非常快。因为它们保存在内存中,所以它们只是暂时的,如果您没有删除它们,它们会在重新启动时消失。另一个优点是,作为普通文件,它们不是 PHP 特有的技术,因此可以根据需要从其他软件访问。您可以像访问普通文件和流一样访问 tmpfs 文件系统上的文件;它们在内存中的事实对 PHP 脚本是透明的。前面的基于文件的例子在 RAM 磁盘上运行得非常好。
要在 Linux 上创建 tmpfs 文件系统,首先在磁盘上创建一个目录,用于将内存设备“连接”到您的文件系统。然后在该位置安装内存设备并开始使用。清单 5-11 中的 shell 脚本(清单 5-12 中的输出)给出了一个安装和移除 tmpfs RAM 磁盘的示例。
#!/usr/bin/env bash
mkdir /tmp/myMemoryDrive
sudo mount -t tmpfs /mnt/tmpfs /tmp/myMemoryDrive
php -r "file_put_contents('/tmp/myMemoryDrive/test.txt',\"Hello\n\");"
cat /tmp/myMemoryDrive/test.txt
sudo umount /mnt/tmpfs
cat /tmp/myMemoryDrive/test.txt
Listing 5-11.
ramdisk.sh
Hello
cat: /tmp/myMemoryDrive/test.txt: No such file or directory
Listing 5-12.ramdisk-output.txt
在清单 5-11 中,您在/tmp/myMemoryDrive
处创建一个目录来连接内存设备,然后将它安装在那里。您执行一行 PHP 来演示创建一个内存文件,就像创建任何其他文件一样,然后cat
这个文件,它应该输出Hello
。最后,你umount
了设备并试图再次cat
文件,但正如你所料,文件不见了;它永远不会保存到物理磁盘上。您可以使用mount
命令挂载 tmpfs 设备,如前面所示,每次启动系统时或任何您想要使用它们的时候,或者您可以在 fstab 文件中添加一个条目,以便在每次系统启动时自动创建它。无论您以何种方式安装它,当您关闭或重新启动时,请始终记住,它以及其中的所有文件都将被销毁。
由于 tmpfs 以与普通文件系统相同的方式运行,您需要确保设置了相关的文件权限,以允许您的所有应用访问它(或者防止那些不应该干预它的应用访问它)。还要记住,如果您的系统内存不足,可能会发生内存交换到磁盘的情况,因此在这些情况下,您的数据可能会暂时接触到您的硬盘,在某些情况下,之后可能会从磁盘中恢复。始终考虑您选择的任何缓存系统的安全含义。
如果出于性能原因考虑使用 tmpfs 而不是物理硬盘,您还应该记住,现代操作系统(包括现代 Linux)可以使用积极的内存缓存进行磁盘访问。这意味着操作系统透明地将经常读取的基于磁盘的文件缓存到动态分配的未使用的内存中(通常您甚至不知道),以提高明显的物理磁盘性能。在这些情况下,当从 tmpfs 内存磁盘读取一些文件并在其上遍历目录树时,您可能看不到预期的性能提高。写入磁盘和不经常访问的文件通常不会被缓存,所以在这些情况下,tmpfs 仍然可以为您带来预期的优势。
在 Windows 中,没有内置的方法来创建基于内存的文件系统。有各种各样的第三方软件来创建 RAM 磁盘,但这并不是标准化的,大多数应用需要一个 GUI 来在每个系统上手动设置磁盘。如果你仍然感兴趣的话,下面列出的维基百科页面会给你更多的提示。
进一步阅读
维基百科上的第三方 RAM 磁盘软件列表
记忆化的缺点
如你所见,通过记忆化进行缓存通常是一件好事,但正如我母亲常说的,“好东西可以有太多。”默认情况下,开始记忆所有函数的诱惑可能会悄然而至,但是和所有事情一样,首先要考虑一些权衡。您的内存化函数会有一点开销,用于在每次运行时检查缓存版本是否可用,以及获取或存储生成的任何缓存版本。如果您正在进行内存化以加速脚本的执行,并且您的缓存驻留在磁盘上,就像前面的主示例一样,那么磁盘 I/O 的额外时间(与内存存储或实际上许多纯计算函数相比,它通常很慢)可能比运行一个低到中等复杂度的函数要长。当然,如果缓存是为了优化低内存系统,减少对外部 API 的调用次数,或者最小化其他与时间无关的资源使用,这可能是一种可以接受的折衷。
在使用记忆化进行缓存时,需要考虑的另一个问题是,相对于成本而言,某些数据的短暂性是否会限制您从中获得的价值。例如,如果您的函数的参数之一是客户 ID,但是您的客户很少对您的在线商店进行一次以上的访问/购买,则该函数的任何缓存很可能只在那一次访问期间有益。与更一般的缓存情况相比,纯函数内存化的一个好处是,您永远不必担心缓存失效,因为您的缓存永远不会失效。然而,这导致了简单地忘记缓存并让它保持原样的诱惑,从编程的角度来看这是非常好的;您的代码将继续运行良好,输出正确。但是,您的系统管理员可能很快就会过来,开始询问您是否真的需要昂贵的 SAN 上的所有磁盘空间。磁盘空间的成本可能超过脚本的有限加速。在这些情况下,你有三个选择。
- 放弃记忆:接受一些运行时间更长的脚本。
- 缓存到内存或磁盘上的每个会话文件,而不是长期的磁盘存储:这加快了同一访问中的多个调用的速度,但会暂时消耗一些内存。
- 执行某种形式的缓存驱逐:删除缓存的文件,比方说,超过一个月。
懒惰评估
懒惰评估是一种艺术,只做尽可能少的工作就能得到你需要的结果。这对一个 PHP 程序员来说应该是很自然的!考虑下面的伪代码:
- if ( do_something_easy()或 do_something_hard() )
这段代码表示“如果do_something_easy()
或do_something_hard()
为真,则返回。”所以,为了确定你是否应该返回,你可以调用两个函数,如果其中一个返回 true,那么你就知道要返回。然而,考虑到如果do_something_easy()
返回 true,那么do_something_hard()
返回什么并不重要,因为在任何情况下你都会返回。所以,在运行了do_something_easy()
之后,运行第二个函数调用实际上是没有意义的,您可以节省这样做的开销。相反,如果它返回 false,您将需要运行第二个函数,但是这并不比您首先自动调用这两个函数更糟糕。这叫懒评;你只评估你需要的东西,而不是更多的陈述。
PHP 在计算布尔表达式时使用一种称为短路计算的惰性计算,这取决于逻辑运算符的先例。所以,在这里你没有什么要做的,除了记下手册后面的内容,如果你在这样的表达式中调用函数,以确保你没有短路!
进一步阅读
PHP 手册:
- 逻辑运算符:
http://php.net/manual/en/language.operators.logical.php
- 运算符优先级:
http://php.net/manual/en/language.operators.precedence.php
发电机
但是,您可以将这种惰性求值的概念应用到您的函数中,以加快速度。在你在前面章节看到的函数组合的例子中,你通常接受一个数据数组,对它做一些事情,把数组传递给下一个函数,做一些其他的事情,等等。即使您实际上并不需要数组中的所有数据,您通常也会传递整个数组,并对整个数组应用您的函数和转换。你看了一下array_filter
,它使用一些过滤函数将数组的大小减少到特定的元素,但即使这样,过滤函数也应用于数组的每个元素。如果您只需要前 10 个匹配元素,而有 100 个匹配元素,那么在找到前 10 个元素后应用过滤函数就浪费了时间,并且您需要一个额外的步骤,比如使用array_slice
将结果 100 个元素减少到 10 个。
PHP 有一个很有用的语言工具叫做生成器,它是在 PHP 5.5 中引入的。生成器允许您创建返回类似数组的函数,但其数据是在访问元素时“实时”生成的。您可以使用生成器来创建只做最少必要工作的惰性函数。
当您将生成器函数链接在一起时,执行会向后进行。考虑如下三个标准函数的伪链:
array_filter some_function();
array_filter another_function();
array_slice 0, 10;
首先过滤整个数组,然后再次过滤整个结果,然后第二个结果将减少到 10 项。在基于生成器的系统中,您可以像这样编写一个链:
lazy_filter some_function();
lazy_filter another_function();
lazy_slice 0, 10;
它看起来是一样的,但是当您执行它时,这个动作实际上从lazy_slice
开始,它通过链向上拉值。slice 函数从第二个过滤器请求值,直到它有十个值。每次第二个过滤器得到一个值的请求,它从第一个过滤器请求值,并对它们应用another_function()
,直到它有一个匹配。每次第一个过滤器收到一个值的请求时,它从数组中取值,并对它们应用some_function()
,直到得到一个匹配。因此,当lazy_slice
得到它的 10 个值时,两个lazy_filter
函数调用它们的(潜在昂贵的)过滤函数的次数只够产生这 10 个值,而不一定是原始数据的所有项。
一会儿你会看到一个发电机的基本例子。但在此之前,让我们创建一个函数来重复调用一个函数。当您查看计时时,同一台 PC 上不相关的任务可能会暂时降低脚本运行速度。多次运行脚本或函数可以限制这种暂时波动对基准计时数字的影响。参见清单 5-13 。
<?php
# For benchmarking results, it's best to repeatedly run the
# function to minimize the effect of any external slowdowns.
# The following function simply calls a function $func $n times
# with arguments $args, and returns the return value of the last
# call.
$repeat = function ($func, $n, ...$args) {
for ($i=0; $i < $n; $i++) {
$result = $func(...$args);
}
return $result;
};
Listing 5-13.
repeat.php
现在让我们来看一个简单的发电机示例(参见清单 5-14 ,输出如清单 5-15 所示)。生成器是一个函数,它有一个yield
语句,而不是一个return
语句。与返回时会丢失状态的普通函数不同,生成的函数会保持其状态,直到下一次被调用。
PHP 有一个名为range()
的本地函数,它返回一个从$start
到$end
的数字数组,并带有一个可选的$step
值。您将创建一个生成器版本,gen_range()
,它产生相同的输出,但是很慢。您将使用相同的参数调用这两个函数,以生成 1 到 1000 万之间的每四个数字,然后当您得到一个可被 123 整除的数字时,退出您正在运行的函数。
<?php
# Get our repeat function
require('repeat.php');
# PHP's native function range() takes a
# $start int, $end in and $step value, and
# returns an array of ints from $start to $end
# stepping up by $step each time. We'll create
# a generator version that takes the same
# parameters and does the same task, called gen_range()
function gen_range($start, $end, $step) {
for ($i = $start; $i <= $end; $i += $step) {
# yield turns this function into a generator
yield $i;
}
};
# We'll create a function to run either range() or
# gen_range() (as specified in $func) with the
# same paramters, and to iterate through the
# returned values until we find a number exactly
# divisible by 123 (which in this case is 369)
$run = function ($func) {
# Get a range from 1 to ten million in steps of 4,
# so 1,4,9,13,18,...,9999989,9999993,9999997
foreach ( $func(1, 10000000, 4) as $n ) {
if ($n % 123 == 0) {
# exit the function once we've found one, reporting
# back the memory in use (as it will be freed once
# we have returned).
return memory_get_usage();
};
};
};
# A function to get the time/memory use for the runs
$profile = function ($func, ...$args) {
$start = ["mem" => memory_get_usage(), "time" => microtime(true)];
$end = ["mem" => $func(...$args), "time" => microtime(true)];
return [
"Memory" => $end["mem"] - $start["mem"],
"Time" => $end["time"] - $start["time"]
];
};
# Finally let's run each of range() and gen_range() 100 times,
# and output the time taken for each and memory used
Echo "*** range() ***\n";
print_r ( $profile($repeat, $run, 100, 'range') );
Echo "*** gen_range() ***\n";
print_r ( $profile($repeat, $run, 100, 'gen_range') );
Listing 5-14.
generators.php
*** range() ***
Array
(
[Memory] => 134222280
[Time] => 8.9564578533173
)
*** gen_range() ***
Array
(
[Memory] => 4952
[Time] => 0.0016660690307617
)
Listing 5-15.generators-output.txt
所以,你可以看到,懒惰版本使用的内存量比普通的range()
函数少得多。这是因为在用foreach
开始遍历它们之前,range()
必须生成整个值数组,而gen_range()
只保存序列中的当前值。gen_range()
花费的时间也少得多,因为一旦你达到 369,你就完成了,而range()
甚至在你开始之前就必须生成序列中的每一个值。
请注意,使用的内存是当$run
函数返回时memory_get_usage
返回的值,对于您的函数来说,这可能是每个函数中使用的最高内存量。
这就是发电机的样子。现在让我们看看如何在函数组合中使用它们,以最小化函数链所要做的工作量。您将创建一个脚本,它获取名副其实的(公共领域)莎士比亚全集(以纯文本文件的形式),获取提到单词 hero 的行,获取长度超过 60 个字符的行,然后返回前三个匹配项。
清单 5-16 展示了如何以一种非懒惰的方式来做这件事,输出如清单 5-17 所示。
<?php
# Borrow some functions from Chapter 3,
# and our repeat function
require('../Chapter 3/compose.php');
require('../Chapter 3/partial_generator.php');
require('repeat.php');
# A helper function to fix parameters from the right,
# as we'll otherwise call partial(reverse()) a lot below.
$partial_right = function ($func, ...$params) {
return partial(reverse($func), ...$params);
};
# Get the start time, to see how long the script takes
$start_time = microtime(true);
# A function to return true if $word is in $str
# (not comprehensive, but matches a word bounded
# by non-A-Z chars, so matches "hero" but not "heroes")
$match_word = function($word, $str) {
return preg_match("/[^a-z]${word}[^a-z]/i", $str);
};
# A function to return true if $str is longer than $len chars
$longer_than = function($len, $str) {
return strlen($str) > $len;
};
# A partial function, fixing hero as the word to search for
$match_hero = partial($match_word, 'hero');
# Another partial function, picking out strings longer than 60 chars
$over_sixty = partial($longer_than, 60);
# A partial function which uses array_filter to apply $match_hero
# to all elements of an array and return only those with 'hero' in
$filter_hero = $partial_right('array_filter', $match_hero );
# Similarly, we'll filter an array with the $over_sixty function
$filter_sixty = $partial_right('array_filter', $over_sixty );
# A function to grab the first 3 elements from an array
$first_three = $partial_right('array_slice', 3, 0);
# Let's now compose the function above to create a
# function which grabs the first three long
# sentences mentioning hero.
$three_long_heros = compose(
$filter_hero,
$filter_sixty,
$first_three
);
# Finally, let's actually call our composed function 100 times
# on the contents of all_shakespeare.txt
# Note that calling file() as a parameter means that it is
# only evaluated once (and not 100 times), so the time for disk
# IO won't be a major element of our timings
$result = $repeat(
$three_long_heros,
file('all_shakespeare.txt'),
100
);
# Print out the result of the last call (which should be the
# same as all of the rest, as all of our composed functions are
# pure and are called on exactly the same input parameter)
print_r($result);
# and the time taken
echo 'Time taken : '.(microtime(true) - $start_time);
Listing 5-16.
filter.php
Array
(
[0] => Enter DON PEDRO, DON JOHN, LEONATO, FRIAR FRANCIS, CLAUDIO, BENEDICK, HERO, BEATRICE, and Attendants
[1] => Sweet Hero! She is wronged, she is slandered, she is undone.
[2] => Think you in your soul the Count Claudio hath wronged Hero?
)
Time taken : 6.2691030502319
Listing 5-17.filter-output.txt
这给了你你正在寻找的三条线,在我的弱不禁风的笔记本电脑上运行 100 次大约需要 6 秒钟。清单 5-18 以一种懒惰的方式重写了这个脚本,输出如清单 5-19 所示。
<?php
# Again we'll borrow some functions from Chapter 3,
# and our repeat function
require('../Chapter 3/compose.php');
require('../Chapter 3/partial_generator.php');
require('repeat.php');
# and start timing
$start_time = microtime(true);
# We'll now define a lazy version of array_filter, using
# a generator (note the yield statement)
$lazy_filter = function ($func, $array) {
# Loop through the array
foreach ($array as $item) {
# Call the function on the array item, and
# if it evaluates to true, return the item
if ( $func($item) ) { yield $item; }
};
};
# The following functions are exactly the same as
# in the non-lazy filter.php example
$match_word = function($word, $str) {
return preg_match("/[^a-z]${word}[^a-z]/i", $str);
};
$longer_than = function($len, $str) {
return strlen($str) > $len;
};
$match_hero = partial($match_word, 'hero');
$over_sixty = partial($longer_than, 60);
# Our $filter_hero function is almost the same,
# but note that it calls $lazy_filter instead of
# array_filter (and it uses partial() rather than
# $partial_right, as I've implemented $lazy_filter
# with the parameters in the opposite order to
# array_filter.
$filter_hero = partial($lazy_filter, $match_hero );
# Again $filter_sixty uses $lazy_filter rather than array_filter
$filter_sixty = partial($lazy_filter, $over_sixty );
# As the output from filter_sixty will be a generator object
# rather than an array, we can't use array_slice to
# get the first three items (as data doesn't exist in a
# generator until you call for it). Instead, we'll create
# a $gen_slice function which calls the generator $n times
# and returns the $n returned values as an array. We'll take
# advantage of that fact that a generator is an iterable object,
# and so has current() and next() methods to get each value.
# We'll practice our recursion, rather than just using
# a for loop!
$gen_slice = function ($n, $output = [], $generator) use (&$gen_slice) {
$output[] = $generator->current();
$generator->next();
if ($n > 1) {
$output = $gen_slice(--$n, $output, $generator);
}
return $output;
};
# $first_three uses $gen_slice rather than array_slice
$first_three = partial($gen_slice, 3, []);
# We'll compose them together, repeatedly call them
# and output the results using exactly the same
# code as in the non-lazy version
$three_long_heros = compose(
$filter_hero,
$filter_sixty,
$first_three
);
$result = $repeat( $three_long_heros, file('all_shakespeare.txt'), 100 );
print_r($result);
echo 'Time taken : '.(microtime(true) - $start_time);
Listing 5-18.
lazy_filter.php
Array
(
[0] => Enter DON PEDRO, DON JOHN, LEONATO, FRIAR FRANCIS, CLAUDIO, BENEDICK, HERO, BEATRICE, and Attendants
[1] => Sweet Hero! She is wronged, she is slandered, she is undone.
[2] => Think you in your soul the Count Claudio hath wronged Hero?
)
Time taken : 2.1842160224915
Listing 5-19.lazy_filter-output.txt
你得到了同样的结果,但是只用了微不足道的两秒钟,大约快了三倍。那么,这是如何工作的呢?嗯,你的lazy_filter
不返回任何数据,而是“产生”一个生成器对象。该对象实现了 PHP 的迭代器接口,因此像foreach
这样的函数自动知道如何使用它,就像它是任何其他可迭代的数据类型一样。当您使用gen_slice()
函数时,这一点变得非常明显,它不是假装您正在使用一个数组,而是简单地调用生成器对象的current()
和next()
方法来请求接下来的三段数据。如果你不熟悉迭代器,PHP 手册的下一节将会帮你解决。
进一步阅读
- PHP 手册中的
Iterator
类
顺便说一句,当我写前面的脚本时,我从compose
语句开始,命名它链接在一起的三个函数,然后向后工作,找出实现它们需要什么函数。这是您在函数式编程时经常使用的模式;声明性的本质适合于程序设计的自顶向下的方法。
懒惰评估的缺点
生成器很棒,一般来说惰性评估是一个非常有用的工具。然而,正如你所料,值得注意的是,这也有不好的一面。如果您再次运行您的generators.php
示例,但是这次不是寻找一个可被 123 整除的数,而是使用值 9999989,清单 5-20 和清单 5-21 显示了会发生什么。
*** range() ***
Array
(
[Memory] => 134222280
[Time] => 26.05708694458
)
*** gen_range() ***
Array
(
[Memory] => 4952
[Time] => 41.604923009872
)
Listing 5-20.generators2-output.txt
标准的range()
函数需要 26 秒,但是你的懒惰的gen_range()
函数几乎翻倍,达到 41 秒。为什么呢?嗯,发电机有一个固有的开销。寻找一个能被 9999989 整除的数(在这种情况下,就是它本身)意味着你必须一直走到数列的末尾才能找到它。但是你必须对序列中的每个数字调用一个函数(通过foreach
),而不是对range()
调用一个函数,并且每个函数调用都有少量的开销。此外,您调用的函数是由您用 PHP 编写的,而不是由整个 PHP 核心开发团队用 C 编写的,因此不太可能是高度优化的代码。因此,通常会有这样一个时刻,生成器的时间效率比首先进行完整的评估要低。它通常是最小的,并且接近评估过程的末尾,如果您的运行有一个均匀的输入值“分布”,那么您通常会在总体上领先,即使有几个确实比完整的评估方法花费更长的时间。不过,考虑您的用例,并根据真实世界的数据来分析您的代码总是值得的。
不过,也不全是坏消息。如果您看一下内存使用数据,您会发现它们与第一个示例中寻找可被 123 整除的数字完全相同。在这种情况下,如果您在内存受限的设备上工作,您可能会认为每次改变值(而不是预先生成它们)所导致的内存减少值得偶尔的额外执行时间。
并行程序设计
在漫长的写书过程中,我常常希望我的每只手都能同时写下不同的章节;那样我会以两倍的速度完成这本书。不幸的是,当我意识到我弱小的大脑一次只能记住一组单词时,我狡猾的计划受挫了。幸运的是,现代计算机不像我这样受限,可以同时执行和跟踪许多任务。计算机以各种方式做到这一点(并行计算、多任务处理、多线程、多重处理等)。),但都归结为一点:同一时间做的越多,完成事情越快。
然而,事情并不都是美好的,即使当你在同一时间做不同的事情时,现代个人电脑的智能也能让事情井井有条。资源争用、死锁、竞争条件:这些都是当多个线程或进程试图访问相同的资源(变量、数据、文件、硬件等)时发生的事情。)同时。也许像这样编程最难的部分是考虑当你的脚本在不同的路径上执行时可能发生的所有可能性。
函数式编程可以使这变得更容易。当你的程序需要做并行任务的时候,他们会剥离一些线程、子进程或者类似的来完成任务,他们往往会在线程或者进程返回的时候,把结果组合起来或者采取一些行动。如果您使用本书中介绍的功能原则编写这些任务工人,每个任务工人都可以成为一个纯函数链,其中:
- 任务只依赖于给定的输入(比如函数的参数),而不依赖于任何外部状态。
- 这项任务可以很容易地单独推理,因为它不受其他任务的影响。
这意味着你不必担心(太多)其他任务正在做什么,它们可能正在使用哪些你想要的资源,等等。当您的任务被调用时,它需要的所有东西都作为输入的一部分提供,并且它返回它的输出供父脚本处理/存储等。即使严格来说它不是一个函数,您也可以像它一样编写您的 worker 脚本,接受来自父级的输入,就像它是参数一样,并在最后像返回值一样向父级返回一个值。
PHP 并不适合并行编程,但是有许多方法可以实现并行计算,在需要时可以付诸实施。也许最简单的方法是使用 PHP 内置的进程控制函数并行启动多个 PHP 脚本来完成这项工作。让我们看一个以这种方式使用过程控制的例子。
你要创建一个程序,对莎士比亚全集做一些分析。您将创建一个以正常的线性方式进行分析的函数,以及一个生成多个“客户端”PHP 工作脚本来并行进行分析的函数。首先你会看到你的主parallel.php
控制脚本,然后你会看到在并行版本中使用的client.php
脚本,最后你会看到functions.php
脚本,它包含了各种分析和并行化功能。您的脚本将从文本中挑选出满足特定条件的单词,对这些单词在整个文本中出现的次数进行求和,然后报告该集合中出现的前十个单词。您将重复每个函数 100 次来对它们进行基准测试。
<?php
# Get a set of functions that we'll look at shortly
require('functions.php');
# The text to work on.
$shakespeare = file_get_contents('all_shakespeare.txt');
# How many times we're going to run each function, for
# benchmarking purposes
$repeats = 100;
# Compose our single process "standard" function.
$analyze_single = compose(
$only_letters_and_spaces, # simplify the text
'strtolower', # all lowercase, please
$analyze_words, # do the analysis
$sort_results, # sort the results
'array_reverse', # get the results in descending order
$top_ten # return the top ten results
);
# Run the single process version $repeats time on $shakespeare input
# Time the runs
$checkpoint1 = microtime(true);
print_r( $repeat($analyze_single, $repeats, $shakespeare) );
$checkpoint2 = microtime(true);
# Now create a parallel process version
$analyze_parallel = compose (
$launch_clients, # Launch a set of client processes to do
# the analysis
$report_clients, # Tell us how many clients were launched
$get_results, # Get the results back from the clients
$combine_results, # Combine their results into one set
$sort_results, # sort the combined results
'array_reverse', # get the results in descending order
$top_ten # return the top ten results
);
# Run the parallel version and time it
$checkpoint3 = microtime(true);
print_r ( $repeat($analyze_parallel, $repeats, $shakespeare) );
$checkpoint4 = microtime(true);
# Finally, dump the timings for comparison
var_dump( 'Single : '.($checkpoint2 - $checkpoint1));
var_dump( 'Parallel : '.($checkpoint4- $checkpoint3));
Listing 5-21.
parallel.php
在$analyse_parallel
组合中,$launch_clients
函数将并行启动清单 5-22 中脚本的多次运行。
<?php
require('functions.php');
# Get the chunk of text for the client to analyze
# by reading the contents of STDIN which are piped to
# this script by the fwrite($clients[$key]["pipes"][0], $string)
# line in the $launch_clients function in the parent process
$string = stream_get_contents(STDIN);
# Compose a function to do the analysis. This is the same
# as the first three steps of the single process analysis
# function, with a step to encode the results as JSON at
# the end so we can safely pass them back
$client_analyze = compose(
$only_letters_and_spaces,
'strtolower',
$analyze_words,
'json_encode'
);
# Run the function and write the results to STDOUT,
# which will be read by the stream_get_contents($client["pipes"][1])
# line in the $get_results function in the parent process. In most cases
# you can use echo to write to STDOUT, but sometimes it can be
# redirected, and so explicitly writing like this is better practice
fwrite(STDOUT, $client_analyze($string) );
Listing 5-22.
client.php
最后,清单 5-23 显示了functions.php
脚本,它实现了您在前面的脚本中编写的所有功能。我把它们分开是为了让脚本更容易阅读,同时也是因为两个脚本都可以访问它们。
<?php
# Borrow some utility functions from previous examples
require('../Chapter 3/compose.php');
require('repeat.php');
# To simplify our analysis, replace anything that's not
# a letter with a space.
$only_letters_and_spaces = function($string) {
return preg_replace('/[^A-Za-z]+/', ' ', $string);
};
# This is the "expensive" deliberately un-optimized function
# that does our "analysis".
$analyze_words = function ($string) {
# Split our text into an array, one word per element
$array = preg_split('/ /i', $string, -1, PREG_SPLIT_NO_EMPTY);
# Filter our array for words that...
$filtered = array_filter($array, function ($word) {
return (
# ... contain any of the letters from the word shakespeare
preg_match('/[shakespeare]/', $word) != false)
# ... AND has at least 1 character in common with this sentence
&& (similar_text($word, 'William is the best bard bar none') > 1)
# ... AND sound like the word "bard"
&& (metaphone($word) == metaphone('bard'))
# ... AND have more than three characters in them
&& ( (strlen($word) > 3 )
);
});
# Finally, count up the number of times each of the filtered
# words appears in the analyzed text, and return that
return array_count_values($filtered);
};
# Slice the top 10 items off the top of the array
$top_ten = function ($array) {
return array_slice($array, 0 ,10);
};
# Sort the results numerically
# asort mutates the array, so we wrap it in a function
$sort_results = function($array) {
asort($array, SORT_NUMERIC);
return $array;
};
# The following functions manage the execution of parallel client scripts
# A function to split the text into chunks and launch the
# appropriate number of clients to process it
$launch_clients = function ($string) {
# Split the string into chunks of 1 million characters,
# a value which I found by trial and error to give the
# best results on this machine for this process
$strings = str_split($string, 1000000);
# An array to hold the resource identifiers for the client scripts
$clients = [];
# Descriptors for "pipes" to read/write the data to/from our client
# scripts
$descriptors = [
0 => ["pipe", "r"], #STDIN, to get data
1 => ["pipe", "w"] #STDOUT, to send data
];
# Iterate through the chunks...
foreach ($strings as $key => $string) {
# $key will be the array index, 0, 1, 2, 3... etc.
# We'll use it as a handy way to number our clients
# Define the command that runs the client
$command = "php client.php";
# Open the clients with proc_open. This returns a resource identifier.
# We'll store it, although our script won't actually use it.
$clients[$key]["resource"] = proc_open( $command,
$descriptors,
$clients[$key]["pipes"]
);
# Note the third parameter above is a variable passed by reference.
# This is used by proc_open to store an array of file pointers
# identifying PHP's end of the pipes that are created.
# We use that info here to write our text chunk to. This writes
# it to STDOUT, and our client script reads it in through STDIN
# at its end of the pipe.
fwrite($clients[$key]["pipes"][0], $string);
# Close the pipe now we're done writing to this client.
fclose($clients[$key]["pipes"][0]);
};
# Once all of the clients have been launched, return their
# resource identifiers and pipe details
return $clients;
};
# Simple impure function to report how many clients were
# launched. You could use a writer monad instead if you wanted
$report_clients = function ($clients) {
# The escape code at the end minimizes our output when
# when running the script many times, by going up one line
# and overwriting the output each time.
echo("Launched ".sizeof($clients)." clients\n\033[1A");
return $clients;
};
# A function to get the results back from the clients.
# The clients will send a JSON encoded array back to us
$get_results = function ($clients) {
# An array to gather the results. Each clients' result
# will be stored as an element of the array
$results = [];
# Iterate through the client resource identifiers
foreach ($clients as $key => $client) {
# Clients write output to STDOUT, which corresponds to the
# STDIN Pipe at our end. We'll read that JSON data and
# decode it to a PHP array. Each client's results will be
# stored as a separate element of the $results array.
$results[] = json_decode(
stream_get_contents($client["pipes"][1]),
true);
# We've done reading from the client, so we can close the pipe.
fclose($clients[$key]["pipes"][1]);
};
# And finally return all of the results from all of the clients
return $results;
};
# This function takes the results array from $get_results above and
# combines it into a single array
$combine_results = function ($results) {
# Reduce and return the input array by...
return array_reduce($results, function($output, $array) {
#... iterating over each individual clients results array
# and either creating or adding the count for each word to
# the output depending on whether that word already exists in
# the output
foreach ($array as $word => $count) {
isset($output[$word]) ?
$output[$word] += $count :
$output[$word] = $count ;
}
# return $output through to the next iteration of array_reduce
return $output;
}, []); # starting with a blank array [] as output
};
Listing 5-23.
functions.php
让我们运行parallel.php
看看会发生什么(参见清单 5-24 )。
Array
(
[beard] => 76
[bright] => 43
[buried] => 43
[bred] => 36
[breed] => 35
[bird] => 34
[bride] => 30
[broad] => 15
[bread] => 15
[board] => 15
)
Launched 4 clients
AArray
(
[beard] => 76
[bright] => 43
[buried] => 43
[bred] => 36
[breed] => 35
[bird] => 34
[bride] => 30
[broad] => 15
[bread] => 15
[board] => 15
)
string(24) "Single : 48.808692932129"
string(25) "Parallel : 25.10250711441"
Listing 5-24.parallel-output.txt
正如您所看到的,您从分析的单个过程和并行过程版本中获得了相同的结果,但是并行版本花费了大约一半的时间来执行。像这样对文本进行分块,可以让四个客户端进程并行地分析所有文本。考虑到该函数的两个版本使用了完全相同的昂贵函数($analyze_words
),您可能会奇怪为什么在四个客户端的情况下,它没有在四分之一的时间内完成。原因是并行运行时有大量的设置工作要做,包括:
- 将文本分成几大块
- 启动新的 PHP 进程
- 写入和读取过程管道
- 最后将结果组合在一起
因此,如果您想进一步加快速度,难道不能简单地并行启动更多的客户端吗?让我们试一试,将文本分成 100,000 个字符的块,这需要 38 个客户端并行计算(参见清单 5-25 )。
Array
(
[beard] => 76
[bright] => 43
[buried] => 43
[bred] => 36
[breed] => 35
[bird] => 34
[bride] => 30
[broad] => 15
[bread] => 15
[board] => 15
)
Launched 38 clients
Array
(
[beard] => 76
[bright] => 43
[buried] => 43
[bred] => 36
[breed] => 35
[bird] => 34
[bride] => 30
[broad] => 15
[bread] => 15
[board] => 15
)
string(24) "Single : 49.230798959732"
string(26) "Parallel : 145.74519586563"
Listing 5-25.parallel-output2.txt
在这种情况下,您的速度从两倍增加到将近三倍长!这也是因为协调所有客户端并将结果汇集在一起的开销。因此,使用这种技术,在给出最大结果的并行进程的数量上,通常有一个最佳点。这在很大程度上取决于手头的任务,对于具有以下特征的函数,您可能会获得更好的结果:
- 结果不需要大量后处理的函数(例如,来自不同客户端的结果的顺序或内容无关紧要)
- 设置成本低廉的功能(例如,拆分输入数据的最少处理,向客户端传输的最少数据)
- 运行时间较长的函数(与函数执行时间相比,时间开销最小)
如您所见,如果没有大量额外的代码来管理并行化,速度就不会提高。在进入代码并行化阶段之前,您可以做许多事情来加快执行速度,包括:
- 使用惰性评估,首先对单词进行计数和排序(廉价操作),然后将分析作为生成器函数的一部分进行应用
- 重新排序
array_filter
中的操作,以利用 PHP 的惰性求值,在调用更昂贵的preg_match
之前,先用便宜的函数如strlen
来缩减数据 - 预先计算
metaphone('bard')
并存储在变量中,而不是每次都计算 - 用更便宜的
strpbrk
PHP 函数替换preg_match
如果这还不足以达到您的性能目标,并且您需要进行并行,那么您可以做一些其他的事情来加速并行版本(为了保持代码简单并节省本书的篇幅,我没有这样做)。
- 在每个脚本中只包含您需要的函数,也许使用一个构建步骤来内联它们。
- 直接在共享内存中传递数据,而不是通过管道,这样会更快。
- 不要等到每个客户端都发送了数据之后才继续从下一个客户端读取数据,要以非阻塞的方式反复循环,直到每个客户端都为您准备好了数据。
对于并行脚本来说,惰性评估可能很困难,因为每个脚本都按照适合其本地输入的顺序返回数据,而不一定代表整个数据。例如,使用这个脚本,每个客户端都可以计算自己的最佳结果,但是您不能只接受收到的前十个结果,因为它们可能不是整个莎士比亚作品的前十个结果,而仅仅是那些首先被分析和返回的数据块。正如您所看到的,并行化工作需要一些思考,即使函数式编程通过消除考虑副作用的额外负担来帮助您。也考虑到我甚至还没有谈到如果你的一个客户没有完成或者挂起该怎么办,你就会明白为什么你应该只在真正必要的时候才考虑这种技术。
多线程编程
多线程编程的工作方式类似于您在上一节中看到的多进程示例。关键的区别在于并行执行发生在同一个进程中,而不是在不同的进程中。PHP 不是多线程的;但是,使用 Pthreads 扩展可以实现多线程。Pthreads 是一个健壮的基于 OOP 的实现,性能比多进程脚本要好得多;然而,由于共存于同一进程中的线程的性质,它比多进程代码实现起来更复杂。还要注意,Pthreads 扩展只能用于 PHP 的“线程安全”版本,这与许多 PHP 扩展不兼容。Linux 上的大多数包管理器不包含线程安全版本,因此需要您手动编译 PHP(如果您想了解自己编译 PHP 的信息,请参见附录 A),或者对于 Windows,您需要从 PHP 网站下载线程安全的可执行文件。
尽管如此,采用前面介绍的函数式编程原则将有助于您避开多线程编程中常见的一些问题。关于扩展和使用示例的更多信息可以在 Pthreads 网站上找到。
进一步阅读
- Pthreads 网址:
http://pthreads.org/
- PHP 手册中的 Pthreads 部分:
http://php.net/manual/en/book.pthreads.php
标准 PHP 库(SPL)
在这一章的开始,我讨论了一个事实,即 PHP 的一些明显的性能问题是由于为用户提供易于使用和通用的数据结构和函数所必需的开销。如果您发现这种开销开始限制您的脚本,那么标准 PHP 库(SPL)是一个核心 PHP 扩展,包含常见和深奥的数据结构和函数。这些都是为解决常见的编程问题而设计的,尽管比 PHP 更常见的结构(如普通的 PHP 数组类型)需要更多的思考。在 SPL 中没有什么是函数式编程独有的,而是有一些有用的函数和结构可以用在你在本书中看到的函数式技术中。
因此,举例来说,如果您发现传递大型数据数组会导致您的脚本达到内存极限,那么您可能希望查看一下SplFixedArray
类。它有一些限制(您只能使用整数作为索引,并且必须预先指定数组的长度),但它提供了一个比普通数组使用更少内存的更快的实现。如果您不熟悉 SPL 中的一些数据结构(如堆、链表等)。),那么最基本的计算机科学入门(或者用更传统的语言编程)应该能帮到你。SPL 还包含用于常见的基于迭代器的任务的函数和类,您可以将这些函数和类与您之前看到的生成器一起使用。
清单 5-26 中的示例脚本让您领略了iterator_to_array
函数、SplFixedArray
结构和FilterIterator
类。
<?php
# Borrow our simple generator example
function gen_range($start, $end, $step) {
for ($i = $start; $i <= $end; $i += $step) {
yield $i;
}
};
# Call the generator...
$gen_obj = gen_range(1,10,1);
# ... and check what we have is a generator object
print_r($gen_obj);
# Generators are iterators, so when we need a full array
# of data instead of a generator, we can convert
# it to an array using SPL's iterator_to_array function
$array = iterator_to_array($gen_obj);
print_r($array);
# An SplFixedArray is SPLs fixed size array data structure.
# Let's create an empty SPL fixed array and a standard PHP array.
# Note we need to specify a size for the SPL array
$spl_array = new SplFixedArray(10000);
$std_array = [];
# Let's create a function to fill an array with data. As both
# array types can be written to in the same way, we can
# use the same function here for both
$fill_array = function($array, $i = 0) use (&$fill_array) {
# recursively fill the $array with data
if ($i < 10000) {
$array[$i] = $i * 2;
return $fill_array($array, ++$i);
};
return ($array);
};
# Let's do some operations with the arrays. We'll measure
# the memory in use before and after each operation.
$mem1 = memory_get_usage();
# Fill the standard array with data
$std_array = $fill_array($std_array);
$mem2 = memory_get_usage(); # 528384 bytes
# Fill the SPL array with data
$spl_array = $fill_array($spl_array);
$mem3 = memory_get_usage(); # 0 bytes
# It took no memory to fill!
# This is because this type of array allocates all of its memory
# up-front when you create it
# Create a new SPL array and fill with data
$spl_array2 = new SplFixedArray(10000);
$spl_array2 = $fill_array($spl_array2);
$mem4 = memory_get_usage(); # 163968 bytes
# This time it did, as we declared it within the section we
# were measuring
# Create a new empty standard array
$std_array2 = [];
$mem5 = memory_get_usage(); # 56 bytes - a small amount
# Create a new empty SPL array
$spl_array3 = new SplFixedArray(10000);
$mem6 = memory_get_usage(); # 163968 bytes - for an empty array!
# This shows that you need to use it with care. A Standard
# array may use more memory for the same amount of data, but
# the memory also shrinks with the array contents too.
echo "Filled Standard Array : ".($mem2 - $mem1). " bytes \n";
echo "1st Filled SPLFixedArray : ".($mem3 - $mem2). " bytes \n";
echo "2nd Filled SPLFixedArray : ".($mem4 - $mem3). " bytes \n";
echo "Empty Standard Array : ".($mem5 - $mem4). " bytes \n";
echo "Empty SPLFixedArray : ".($mem6 - $mem5). " bytes \n";
# The SPL provides various iterator classes that you can extend
# to work with iterable structures like the SPLFixedArray and
# generators
# Let's create a class to filter for values that are divisible by three
class by_three extends FilterIterator {
# We extend the FilterIterator class, and implement the accept() class
# with your filtering function
public function accept()
{
$value = $this->current();
if ($value % 3 == 0) {
# return true to include the value in the output
return true;
}
# or false to filter it out
return false;
}
};
# Let's use it to filter our previous SPL array
$nums = new by_three($spl_array);
var_dump(iterator_count($nums)); # int(3334) (∼third of the array is returned)
Listing 5-26.spl.php
SPL 中还有更多可用的类、函数和数据结构。查看 PHP 手册了解更多细节。
进一步阅读
- PHP SPL 文档
结论
在本章中,您了解了函数式编程在性能改进领域的一些常见应用。即使您没有完全用功能代码编写应用,挑选出导致瓶颈的关键功能,并根据功能原则重写它们,也可以让您将这些性能增强技术应用到这些代码部分。当然,如果你用函数式风格从头开始编写你的应用,当你发现一个有问题的函数时,应用诸如记忆化之类的技术是快速而简单的。
六、使用函数管理业务逻辑
在这一章中,你将会看到函数式编程的其他一些常见用法。您将从了解功能代码如何帮助您管理程序中实现的业务逻辑开始。然后,您将了解什么是基于事件的编程,以及函数式编程如何帮助您处理管理传入事件的复杂性,并保持数据流的畅通。最后,您将快速浏览一下异步编程,并了解为什么函数式编程在该领域也是一个有用的工具。
管理业务逻辑
大多数(有用的)程序执行大量不同的操作,从与数据库对话到生成漂亮的界面屏幕等等。但是定义一个程序做什么的关键是它的“业务逻辑”的实现业务逻辑(有时称为领域逻辑)是将现实世界的业务规则编码成代码的程序的一部分。例如,在会计程序中,业务规则包括如何加、减和舍入货币值;如何处理销售税和折旧;货币之间如何换算;以及如何将资金从一个账户或分类账转移到另一个账户或分类账。实现这些规则的 PHP(或其他语言)代码就是业务逻辑。诸如创建用户界面、生成 PDF 报告等功能通常不被认为是业务逻辑,尽管在特别重要的业务规则(如舍入精度、监管机构指定的输出格式等)中它们可能是业务逻辑。)都有涉及。虽然它包含单词 business,但是业务逻辑不仅仅适用于商业或金融应用。以 Photoshop 这样的程序为例。Photoshop 中有很多代码处理加载和保存图像、创建用户界面、响应用户鼠标和键盘输入等等。然而,Photoshop 中的业务逻辑由应用变换、滤镜、绘画和工具操作等的算法组成。这些是艺术家的“商业”任务,当你反转图像或应用抖动时,艺术家会有“规则”(或期望)。一个软件的书面规范通常从软件需要实现的业务规则开始。
与其他代码相比,业务逻辑有一些特殊的需求。
- 业务逻辑需要是可测试的:如果你的会计网站不小心使用了 Arial 字体而不是 Verdana,这不太可能是什么大问题。但如果它将数百万笔交易四舍五入到最接近的整数,而不是整数,有人就会被解雇。测试代码会带来开销,如果这是一个问题,那么识别关键的业务逻辑来集中有限的测试时间通常是一个聪明的举动。使业务逻辑易于测试,可以最大限度地利用有限的测试资源。
- 业务逻辑需要集中起来,封装成小的单一用途的代码段:以增值税和商品及服务税等销售税为例,根据所讨论的产品和买卖双方的所在地,销售税可以有不同的百分比。如果英国政府决定将欧盟公民的计算机图书增值税从 0%提高到 10 %,但不包括非欧盟公民,图书零售商将需要确保新税率的计算及其在网站产品页面、购物篮页面、结账页面、电子邮件确认、卡处理系统和后端会计系统上的正确应用。在一个位置指定税率,并且具有确定购买者位置和产品类型的单一目的的集中功能,这确保了变化同时反映在系统的所有部分中,并且确保了对不相关的代码(或税率等)产生意外影响的机会。)被最小化。
- 业务逻辑需要清楚地映射到现实世界的业务规则上,以便清楚地了解它们之间的相互关系:如果您能够阅读英语业务规则,并同时遵循代码实现,那么就更容易验证它们是否都被实现了。
- 业务逻辑需要对失败具有弹性:如果网站无法从用户设置中加载用户喜欢的文本颜色,默认的黑色文本通常只会让那些希望使用柔和的炭灰色字体的用户感到些许烦恼。如果同一个网站未能加载其电子商务页面的税率数组,对所有销售应用零百分比税将会使税务部门非常不高兴。
函数式编程,正如我希望你现在已经感受到的,可以帮助你管理你的业务逻辑并满足这些需求。倒过来看前面三点,下面是它的作用:
- 消除副作用可以提高对某些类型的失败的恢复能力,使用像 Maybe monad 这样的结构可以帮助你处理其他问题。
- 我谈到函数式编程是一种“声明性”的编码风格。这意味着你以一种描述你正在做“什么”的方式编码,而不是描述你正在“如何”做(至少在更高层次的组合功能上)。这种声明式风格使得将现实世界的业务规则与其对应的代码相匹配变得更加容易,并且可以同时关注两者。
- 正如您所看到的,函数式编程鼓励您将代码分解成单一用途的函数,然后将函数链和部分函数构建成易于阅读(和推理)的代码块。这鼓励代码重用,并且在函数中封装值(例如,税率)有助于鼓励不变性。
- 函数式编程使测试(如单元测试)变得容易。
因此,即使您的整个程序不是以函数式风格编写的,识别您的关键业务逻辑并使用函数式编程实现它仍然可以给您带来好处。我将在下一章进一步讨论将其他编程范例与函数式代码混合和匹配。但是现在让我们看一个简单的例子,将关键业务逻辑封装在纯函数中。
下面的例子实现了一个假想的电子商务网站所使用的一些(非常简化的)关键财务逻辑。它分为三个文件:business_data.php
、business_logic.php
和shopping.php
。第一个和第二个包含您的集中式业务逻辑。您可能想知道为什么要在business_data.php
文件中创建函数来返回数据(产品价格、税率等)。)而不仅仅是提供静态数组或变量(假设这是函数返回的内容)。将它们创建为函数使您可以在以后灵活地用函数替换它们,例如,根据更复杂的公式调整数据,或者从其他函数或源生成或收集数据。“但是你可以运行这样一个函数并用输出替换变量/数组,”你哭着说,但是这将阻止你使用诸如生成器(如果你不熟悉的话,见第五章)或其他类似的可迭代结构的宝石。使用数据的代码已经设置为调用函数来获取数据,所以您只需要在将来修改函数的实现(当然,除非您需要添加新的参数)。
当您浏览这些函数时,您会注意到您已经将它们中的大部分实现为闭包,函数“use
”调用其他函数。像这样使用闭包的一个优点是,它有助于保持所用函数的不变性。例如,如果在您的调用代码中,您意外地将另一个函数(例如,总是返回零)赋给了$get_tax_rate
,已经创建的使用$get_tax_rate
的闭包将不会受到影响,因为闭包“关闭”或封装了在闭包创建时而不是执行时赋给$get_tax_rate
的“值”(函数)。
为了简化示例,我将省略任何错误检查或清理代码(与本书中的大多数示例一样),但是在现实世界中,在开始处理数据之前,通常值得使用“guard”函数来检查来自经过仔细测试的完全纯业务逻辑内部的有效数据。像 Maybe monad 这样的结构也可以用来处理常见的故障模式。
所以,事不宜迟,让我们看看代码。第一个文件如清单 6-1 所示,包含一些业务逻辑数据。
<?php
# First let's create core business data.
# Rather than just define arrays, we're going to create functions
# that return arrays. We'll discuss why in the chapter.
# Every sale is either local, within our own country, or beyond
$locations = function () {
return ['local', 'country', 'global'];
};
# Each category of products that we sell has a different tax rate,
# and that rate varies depending on where our purchaser is located
$rates = function () {
return [
'clothes' => ['local' => 0, 'country' => 5, 'global' => 10],
'books' => ['local' => 0, 'country' => 5, 'global' => 5],
'cheeses' => ['local' => 20, 'country' => 17.5, 'global' =>2]
];
};
# A list of our products, with their category and price
$products = function () {
return [
'T-shirt' => [ 'Category' => 'clothes', 'Price' => 15.99 ],
'Shorts' => ['Category' => 'clothes', 'Price' => 9.99 ],
'The Dictionary' => ['Category' => 'books', 'Price' => 4.99 ],
'War and Peace' => ['Category' => 'books', 'Price' => 29.45 ],
'Camembert' => ['Category' => 'cheeses', 'Price' => 3.50 ],
'Brie' => ['Category' => 'cheeses', 'Price' => 7.00 ]
];
};
# We only sell in dollars, but we format the prices differently
# depending on the location of the purchaser.
$price_formats = function () {
return [
'local' => ['symbol' => '$', 'separator' => '.'],
'country' => ['symbol' => '$', 'separator' => '.'],
'global' => ['symbol' => 'USD ', 'separator' => ',']
];
};
Listing 6-1.
business_data.php
清单 6-2 中显示的下一个文件包含一些关键的业务逻辑功能。
<?php
# Now we're going to create a set of functions which describe our business
# logic. We're going to keep them as simple as possible, and reference
# other functions within this file where possible to keep a
# "single source of truth" for when we need to update them.
# Load our business data
require('business_data.php');
# Fetch the details of a single product from the list of products
$get_product_details = function ($product) use ($products) {
return $products()[$product];
};
# Get the category name from the details of a single product
$get_category = function ($product_details) {
return $product_details['Category'];
};
# Get the tax rate for a category of products based on the location
# of the purchaser
$get_tax_rate = function ($category, $location) use ($rates) {
return $rates()[$category][$location];
};
# Get the net (tax exclusive) price of a product by name.
$get_net_price = function ($product) use ($get_product_details) {
return $get_product_details($product)["Price"];
};
# Roll the above functions together to create a function that gets
# the gross (tax inclusive) price for a certain quantity of products
# based on the location of our purchaser.
# Note that the tax is rounded using the PHP_ROUND_HALF_DOWN constant
# to indicate the particular rounding method.
$get_gross_price = function ($product, $quantity, $location) use
($get_net_price, $get_tax_rate, $get_category, $get_product_details) {
return round(
$get_net_price($product) *
$quantity *
( 1 + (
$get_tax_rate(
$get_category(
$get_product_details($product)
),
$location)
/100
)
),
2, PHP_ROUND_HALF_DOWN) ;
};
# A function to get the actual amount of tax charged. Note that this doesn't
# simply use the tax rate, as the actual amount charged may differ depending on
# the rounding performed and any future logic added to $get_gross_price.
# Instead we call $get_net_price and $get_gross_price and return the difference.
$get_tax_charged = function ($product, $quantity, $location) use
($get_gross_price, $get_net_price) {
return $get_gross_price($product, $quantity, $location) -
( $quantity * $get_net_price($product) );
};
# Finally, a function to format a string to display the price, based
# on the purchasers location.
$format_price = function ($price, $location) use ($price_formats) {
$format = $price_formats()[$location];
return $format["symbol"] . str_replace('.',
$format["separator"],
(string) $price
);
};
Listing 6-2.
business_logic.php
最后,清单 6-3 展示了一组使用业务逻辑的常见业务任务。实际上,这些可能被分割到许多不同的脚本和系统上,尽管它们都“需要”相同的业务逻辑脚本。
<?php
# Import our set of pure functions which encapsulate our business logic.
require('business_logic.php');
# Now we can use them in our not so pure, not so functional code, safe in the
# knowledge that they (should) provide us with consistent, correct results
# regardless of what we do to the global or external state here.
# Let's generate a shopping cart of products for a user in Bolivia
$cart = ['Brie' => 3, 'Shorts' => 1, 'The Dictionary' => 2 ];
$user = ["location" => 'global'];
# One common function is to list the contents of the cart. Let's do
# that here
echo "Your shopping cart contains :\n\n";
echo "Item - Quantity - Net Price Each - Total Price inc. Tax\n";
echo "=======================================================\n\n";
foreach ($cart as $product => $quantity) {
$net_price = $get_net_price($product);
$total = $get_gross_price($product, $quantity, $user["location"]);
echo "$product - $quantity - $net_price - $total \n";
};
echo "=======================================================\n\n";
# In a confirmation e-mail we may want to just list a (formatted) total price...
$total_price = array_reduce( array_keys($cart),
# loop through the cart and add gross price for each item
function ($running_total, $product) use
( $user, $get_gross_price, $cart ) {
return $running_total +
$get_gross_price( $product,
$cart[$product],
$user["location"]);
}, 0);
echo "Thank you for your order.\n";
echo $format_price($total_price, $user["location"]).' will ';
echo "be charged to your card when your order is dispatched.\n\n";
# And on the backend system we may have a routine that keeps details of
# all the tax charged, ready to send to the Government. Let's create a
# summary of the tax for this order.
$tax_summary = array_reduce( array_keys($cart),
# Loop through each item and add the tax charged to the relevant category
function ($taxes, $product) use
( $user, $get_tax_charged, $cart, $get_category, $get_product_details ) {
$category = $get_category($get_product_details($product));
$tax = $get_tax_charged($product, $cart[$product], $user["location"]);
isset($taxes[$category]) ?
$taxes[$category] =+ $tax : $taxes[$category] = $tax;
return $taxes;
}, []);
echo "Tax Summary for this order :\n\n";
var_dump($tax_summary);
Listing 6-3.
shopping.php
清单 6-4 显示了输出。
Your shopping cart contains :
Item - Quantity - Net Price Each - Total Price inc. Tax
=======================================================
Brie - 3 - 7 - 21.42
Shorts - 1 - 9.99 - 10.99
The Dictionary - 2 - 4.99 - 10.48
=======================================================
Thank you for your order.
USD 42,89 will be charged to your card when your order is dispatched.
Tax Summary for this order :
array(3) {
["cheeses"]=>
float(0.42)
["clothes"]=>
float(1)
["books"]=>
float(0.5)
}
Listing 6-4.shopping-output.txt
像这样构造您的业务逻辑也使得扩展它变得更容易。这个例子是一个虚构的美国零售商(从货币符号可以看出!)从一个网站向全世界销售。然而,在做了一些市场调查后,零售商发现它可以通过制作一个专门的欧洲网站来增加其欧洲销售额,推动布里干酪(针对喜欢奶酪的法国人)和字典(因为英国人不会说话)的销售。鉴于新网站上的所有交易都将遵循“全球”税率,他们可以通过创建一个名为$get_eu_tax_rate
的部分函数来简化代码,将位置固定为global
。像这样扩展逻辑,而不是重写它,意味着他们仍然可以与使用这种通用业务逻辑的现有后端系统紧密集成,当他们在美国扩展奶酪范围以包括 Monterey Jack 时,很容易向欧洲人宣传它是现代奶酪汉堡的装饰。
基于事件的编程
当您编写基于 PHP 的网页时,web 服务器会调用您的脚本来响应用户的请求。这些请求是事件,你永远不知道它们什么时候来,什么页面会被请求,以什么顺序,来自哪个用户。您编写脚本来处理单个特定的页面请求和 web 服务器(Apache、Nginx 等)。)负责管理所有的输入事件,为每个事件调用相关的脚本,并将输出返回给正确的浏览器。您的脚本只“看到”它被调用的当前请求,而不需要(例如)计算出它需要将输出发送回潜在的许多并发访问者中的哪一个。它不需要将每个脚本的状态与其他脚本(或同一脚本的实例)分开,这些脚本可能在它自己执行之前、期间或之后被调用。您的脚本以一种简单的“自顶向下”的过程方式运行,在事件/请求被处理之后,您的脚本就完成了。在这个场景中,web 服务器处理基于事件的编程问题,您的生活很简单!
PHP,作为一种通用的语言,确实让你可以走出这个舒适区,编写你自己的基于事件的脚本。您可以编写长时间运行的脚本,以类似于 Apache 等软件的方式对一组正在发生的事件做出反应并进行处理。有许多方法可以做到这一点,但是在 PHP 中开始基于事件编程的最简单的方法之一是使用 PECL 的“event”扩展。这个扩展是成熟的跨平台libevent
库的包装器。在其核心,libevent
提供了一种将回调函数附加到事件的方法。它可以响应的事件包括信号、文件和网络事件、超时以及任何可以构建在这些基本类型之上的事件。libevent
本机支持的并且 PHP 事件扩展完全包装的事件类型之一是 HTTP 事件。您将使用 HTTP events 编写一个简单的 web 服务器,您将使用它来执行一些数学函数,这样您就可以看到一种管理事件的函数方法,当然还可以像任何优秀的 web 服务器一样提供可爱的猫图片。
那么,为什么要用函数式编程来编写基于事件的程序呢?前面的描述概述了为什么基于事件的编程很难。您不知道您的回调函数将以什么顺序被调用(即,事件将以什么顺序到达您的程序),您需要将来自不同用户的事件分开,但是您仍然需要管理适用于每个用户的状态,这可能会跨越多个事件。您需要在同一个长时间运行的脚本中完成所有这些工作(而不是在每个事件后终止的独立实例)。我相信你能想象为什么用全局或外部状态/变量来管理这样的状态转换会很快变成一个混乱的噩梦来保持你的数据。相比之下,函数式编程教你从一个函数调用到另一个函数调用“沿着链”传递状态,避免使用可变或不纯的外部数据。你的(函数式编程)函数不需要去猜测它们正在处理谁的请求/事件,或者用户当前会话的状态是什么,所有这些信息都作为它们的输入参数传递给它们。对于 HTTP 请求,您可以通过在 HTML 输出中对函数的返回值进行编码,并通过每个 HTTP 请求提供的 URI 参数将这些值作为输入参数接收回来,从而在一系列 HTTP 请求中携带这样的信息。即使事先不清楚哪些函数将以何种顺序被调用,拥有小的、单一用途的函数也可以更容易地推断出程序将做什么。
有两种 PECL 扩展可用于包装libevent
:名副其实的libevent
扩展和更新的event
扩展。我建议使用后者,只是因为它比libevent
扩展更全面,并且仍在积极维护中。要使用event
扩展,你需要用 PECL 安装它,但是首先你需要在你的系统上安装libevent
和它的头文件。清单 6-5 中的安装步骤适用于基于 Debian/Ubuntu 的操作系统;其他操作系统的说明可以在下面的“进一步阅读”部分找到。
# Install the libevent library and it header files
sudo apt-get install libevent-2.0-5 libevent-dev
# Ensure that PECL (which comes as part of the PEAR package)
# and the phpize command which PECL needs are installed
sudo apt-get install php-pear php-dev
# Install the event extension
sudo pecl install event
# Finally make the extension available to the PHP CLI binary
# by editing php.ini
sudo nano /etc/php/7.0/cli/php.ini
# and adding the following line in the section where other .so
# extensions are include
extension=event.so
Listing 6-5.
install_event.txt
进一步阅读
- PHP 手册中的
event
扩展 - 要求和安装信息
- 主
libevent
网站
现在您已经安装了libevent
和event
扩展,您可以编写一个程序来充当 HTTP 服务器并处理传入的 HTTP 请求事件。您将把它分成两个脚本,一个包含您的业务逻辑函数,另一个设置服务器并将函数作为回调连接到传入的请求事件。您将创建一些简单的数学函数(如add()
和subtract()
),它们被映射到 URIs(如/add
和/subtract
),对一个值进行操作,该值从一个请求传递到另一个请求。清单 6-6 展示了你的函数的脚本。
<?php
# We'll create a set of functions that implement the logic that should
# occur in response to the events that we'll handle.
# Use our trusty partial function generator
require('../Chapter 3/partial_generator.php');
# A generic function to output an HTTP header. $req is an object representing
# the current HTTP request, which ensures that our function deals with the
# right request at all times.
$header = function ($name, $value, $req) {
$req->addHeader ( $name , $value, EventHttpRequest::OUTPUT_HEADER );
};
# We are going to be serving different types of content (html, images etc.)
# so we need to output a content header each time. Let's create a
# partial function based on $header...
$content_header = partial($header, 'Content-Type' );
# and then make it specific for each type of content...
$image_header = partial($content_header, "image/jpeg");
$text_header = partial($content_header, "text/plain; charset=ISO-8859-1");
$html_header = partial($content_header, "text/html; charset=utf-8");
# The following function creates a "buffer" to hold our $content and
# then sends it to the browser along with an appropriate HTTP status
# code (Let's assume our requests always work fine so send 200 for everything).
# Note that it's a pure function right up until we call sendReply. You could
# return the EventBuffer instead, and wrap it all into an IO or Writer monad to
# put the impure sendReply at the end if you wish.
$send_content = function($req, $content) {
$output = new EventBuffer;
$output->add($content);
$req->sendReply(200, "OK", $output);
};
# The input parameters for our maths functions are held in the URI parameters.
# The URI is held in the $req request object as a string. Let's get the
# URI and parse out the parameters into an associative array.
$parse_uri_params = function ($req) {
$uri = $req->getUri();
parse_str(
# Grab just the parameters (everything after the ?)
substr( $uri, strpos( $uri, '?' ) + 1 ),
# and parse it into $params array
$params);
return $params;
};
# Get the URI "value" parameter
$current_value = function($req) use ($parse_uri_params) {
return $parse_uri_params($req)["value"];
};
# Get the URL "amount" parameter
$amount = function($req) use ($parse_uri_params) {
return $parse_uri_params($req)["amount"];
};
# A function to send the results of one of our maths functions which follow.
$send_sum_results = function($req, $result) use ($html_header, $send_content) {
# Create some HTML output, with the current result, plus some links
# to perform more maths functions. Note the uri parameters contain
# all of the state needed for the function to give a deterministic,
# reproducable result each time. We also include some links to
# the other utility functions. When you visit them, note that you
# can use your browser back button to come back to the maths functions
# and carry on where you left off, as the parameters the functions
# need are provided by the URI parameters and no "state" has been
# altered of lost
$output = <<<ENDCONTENT
<p><b>The current value is : $result</b></p>
<p><a href="/add?value=$result&amount=3">Add 3</a></p>
<p><a href="/add?value=$result&amount=13">Add 13</a></p>
<p><a href="/add?value=$result&amount=50">Add 50</a></p>
<p><a href="/subtract?value=$result&amount=2">Subtract 2</a></p>
<p><a href="/subtract?value=$result&amount=5">Subtract 5</a></p>
<p><a href="/multiply?value=$result&amount=2">Multiply by 2</a></p>
<p><a href="/multiply?value=$result&amount=4">Multiply by 4</a></p>
<p><a href="/divide?value=$result&amount=2">Divide by 2</a></p>
<p><a href="/divide?value=$result&amount=3">Divide by 3</a></p>
<p><a href="/floor?value=$result">Floor</a></p>
<p><A href="/show_headers">[Show headers]</a>
<a href="/really/cute">[Get cat]</a>
<a href="/close_server">[Close down server]</a></p>
ENDCONTENT;
# Send the content header and content.
$html_header($req);
$send_content($req, $output);
};
# These are our key maths functions. Each one operates like a good Functional
# function by only using the values supplied as input parameters, in this
# case as part of $req. We call a couple of helper functions ($current_value
# and $amount) to help extract those values, $req isn't necessarily
# immutable (we could alter values or call methods), but we'll use
# our discipline to keep it so right up until we're ready to send_contents.
# While we don't formally "return" a value, $send_sum_results effectively
# acts a return statement for us. Any return value would simply go back to
# libevent (which is the caller, and it just ignore it).
# If we want to keep to strictly using explicit return statements, we could
# wrap this in another function that does the same as $send_sum_results, (and
# for the same reason wouldn't have a return statement) or we could create an
# Writer monad or similar to gather the results and only output to the browser
# at the end. For this simple example we'll go with using $send_sum_results
# though for simplicity and clarity.
$add = function ($req) use ($send_sum_results, $current_value, $amount) {
$send_sum_results($req, $current_value($req) + $amount($req) );
};
$subtract = function ($req) use ($send_sum_results, $current_value, $amount) {
$send_sum_results($req, $current_value($req) - $amount($req) );
};
$multiply = function ($req) use ($send_sum_results, $current_value, $amount) {
$send_sum_results($req, $current_value($req) * $amount($req) );
};
$divide = function ($req) use ($send_sum_results, $current_value, $amount) {
$send_sum_results($req, $current_value($req) / $amount($req) );
};
$floor = function ($req) use ($send_sum_results, $current_value) {
$send_sum_results($req, floor($current_value($req)) );
};
# Now we'll define some utility functions
# Grab the HTTP headers from the current request and return them as an array
$get_input_headers = function ($req) {
return $req->getInputHeaders();
};
# A recursive function to loop through an array of headers and return
# an HTML formatted string
$format_headers = function ($headers, $output = '') use (&$format_headers) {
# if we've done all the headers, return the $output
if (!$headers) {
return $output;
} else {
# else grab a header off the top of the array, add it to the
# $output and recursively call this function on the remaining headers.
$output .= '<pre>'.array_shift($headers).'</pre>';
return $format_headers($headers, $output);
};
};
# Use the function above to format the headers of the current request for
# viewing
$show_headers = function ($req) use ($html_header, $send_content, $format_headers) {
$html_header($req);
$send_content($req, $format_headers( $req->getInputHeaders() ) );
};
# Let's handle all requests, so there are no 404's
$default_handler = function ($req) use ($html_header, $send_content) {
$html_header($req);
$output = '<h1>This is the default response</h1>';
$output .= '<p>Why not try <a href="/add?value=0&amount=0">some math</a></p>';
$send_content($req, $output);
};
# Ensure that there are sufficient supplies of cat pictures available
# in all corners of the Internet
$send_cat = function($req) use ($image_header, $send_content) {
# Note we send a different header so that the browser knows
# a binary image is coming
$image_header($req);
# An impure function, you could alway use an IO monad or
$send_content($req, file_get_contents('cat.jpg'));
};
# A function to shut down the web server script by visiting a particular URI.
$close_server = function($req, $base) use ($html_header, $send_content) {
$html_header($req);
$send_content($req, '<h1>Server is now shutting down</h1>');
$base->exit();
};
Listing 6-6.
server_functions.php
清单 6-7 显示了您的脚本,它实际上运行 HTTP 服务器并将早期的函数连接到 URIs。
<?php
# Let's get all of our functions that implement our
# business logic
require('server_functions.php');
# Now we're ready to build up our event framework
# First we create an "EventBase", which is libevent's vehicle for holding
# and polling a set of events.
$base = new EventBase();
# Then we add an EventHttp object to the base, which is the Event
# extension's helper for HTTP connections/events.
$http = new EventHttp($base);
# We'll choose to respond to just GET HTTP requests
$http->setAllowedMethods( EventHttpRequest::CMD_GET );
# Next we'll tie our functions we created above to specific URIs using
# function callbacks. We've created them all as anonymous/closure functions
# and so we just bind the variable holding them to the URI. We
# could use named functions if we want, suppling the name in "quotes".
# with the EventHttpRequest object representing the current request as
# the first paramter. If you need other parameters here for your callback,
# you can specify them as an optional third parameter below.
# Our set of maths functions...
$http->setCallback("/add", $add);
$http->setCallback("/subtract", $subtract);
$http->setCallback("/multiply", $multiply);
$http->setCallback("/divide", $divide);
$http->setCallback("/floor", $floor);
# A function to shut down the server, which needs access to the server $base
$http->setCallback("/close_server", $close_server, $base);
# A utility function to explore the headers your browser is sending
$http->setCallback("/show_headers", $show_headers);
# And a compulsory function for all internet connected devices
$http->setCallback("/really/cute", $send_cat);
# Finally we'll add a default function callback to handle all other URIs.
# You could, in fact, just specify this default handler and not those
# above, and then handle URIs as you wish from inside this function using
# it as a router function.
$http->setDefaultCallback($default_handler);
# We'll bind our script to an address and port to enable it to listen for
# connections. In this case, 0.0.0.0 will bind it to the localhost, and
# we'll choose port 12345
$http->bind("0.0.0.0", 12345);
# Then we start our event loop using the loop() function of our base. Our
# script will remain in this loop indefinitely, servicing http requests
# with the functions above, until we exit it by killing the script or,
# more ideally, calling $base->exit() as we do in the close_server()
# function above.
$base->loop();
# We'll only hit this point in the script if some code has called
# $base->exit();
echo "Server has been gracefully closed\n";
Listing 6-7.
web_server.php
要启动 HTTP 服务器,只需在命令行输入php web_server.php
。您现在可以在 web 浏览器中访问http://localhost:12345
,您将看到默认的响应。单击链接开始使用一些数学函数,并访问实用函数的链接。现在,尝试打开另一个浏览器标签(或者实际上是另一个浏览器)并访问同一个 URL 试着点击一些数学函数和我的猫的可爱照片。在浏览器标签/浏览器之间切换,检查每个页面是否保持正确的状态,不管你在其他标签/浏览器中做什么。因为您将状态作为输入/URL 参数和 HTML 输出中的“返回值”来传递,所以您的状态遵循每个单独的事件流,并且您不需要明确地跟踪用户及其各自的状态。
现在,当然,这是一个玩具问题来说明这个想法,但理想情况下,您可以看到函数式编程中固有的属性如何帮助消除在编写基于事件的程序时跟踪状态的复杂方法的需要。这些不必是网络服务器;任何基于事件的模型(例如,响应系统事件或文件系统变化的程序)都可以受益于函数式编程。
异步 PHP
异步(async)编程是一种编写代码的方式,通过在等待外部 I/O(如数据库调用和磁盘 I/O)完成的同时执行代码,在单线程应用(如 PHP 脚本)中充分利用处理器。由于函数式编程非常适合基于事件的编程的原因,它也是管理异步编程中固有的无序处理的相关复杂性的一个很好的选择。PHP 本身并不支持异步编程,所以我不会在本书中涉及它,但是有几个库确实实现了异步功能,下面将详细介绍。这些库或多或少局限于异步运行 I/O 类型的函数,而不是任意代码(这需要多任务或多线程能力)。脸书的 Hack 语言是 PHP 的一个扩展但基本兼容的实现,具有本机 I/O 异步功能(见下面的链接),但这些功能与主 PHP VM 或任何 PHP 库都不兼容。无论您选择哪种方法,使用本书中概述的功能原则将有助于您对代码的执行进行推理。
进一步阅读
- ReactPHP 可能是 PHP 主要的事件驱动异步库。最初模仿 JavaScript 世界中的 NodeJS,它也是其他异步和事件库的基础。
- ReactPHP 提供了承诺的实现,它基于 JavaScript 承诺模型。
- Hack 提供了一个异步接口,可以选择异步执行某些 I/O 类型的操作。
- Amp 是 PHP 的异步并发框架。
- GitHub 有一个资源、代码、库和其他与 PHP 异步处理相关的材料的列表。
七、在面向对象和过程式应用中使用函数式编程
到目前为止,在本书中,您已经了解了什么是函数式编程的基础知识,以及如何使用它来解决一些常见的编程问题。在这一章中,你将会看到如何把功能代码放到你现有的或者新的应用中。具体来说,您将看到以下内容:
- 如何构建一个功能性的应用,以及是否让它完全发挥作用
- 如何以及何时混合和匹配像函数式编程和面向对象编程这样的范例
在本章结束时,我希望你有信心开始探索甚至在你自己的应用中实现基本的功能策略。
PHP 范例的历史
要理解函数式编程在应用中的位置,通常理解它在 PHP 中的位置是很方便的。谈到编程范例,PHP 是一个大杂烩,这既是一件好事,也是一件坏事。为了理解 PHP 是如何发展成现在这个样子的,你首先需要了解它的历史。早在 1994 年,当拉斯马斯·勒德尔夫第一次创建 PHP 时,它是一种纯粹的过程语言。随着时间的推移,PHP 变得越来越普及,人们开始将它用于越来越大的基于 web 的系统。更大的系统,特别是那些有开发团队而不是单个编码人员的系统,需要不断增加的代码纪律,因此出现了需要面向对象语言特性的学者和专业编码人员。面向对象特性的交付始于版本 3;然而,与此同时,在过程方面有一个更强大和一致的语言语法的推动,所以版本 3 提供了两者。
在那个阶段,基本的 OO 特性被许多人嘲笑(特别是那些来自其他语言如 Java 和 C++的特性),这阻止了许多过程式 PHP 程序员尝试 OOP 功能。直到版本 5,随着 Zend Engine 2 中新的对象模型的实现,才出现了合适的 OO 特性。即使在那时,与对象相关的语法和特性支持也没有达到您当前所拥有的那种程度,直到版本 5 系列的后续版本。这一点,再加上 PHP 开发人员(完全合理地)不愿意打破向后兼容性,意味着 PHP 的过程式特性和它新的 OO 特性一起得到了很好的磨砺。
如果你看一下 PHP 手册中的“PHP 历史”一页,你不会发现任何地方提到函数式编程。函数式编程,作为一个概念,从来都不是 PHP 正式开发路线图的一部分。事实上,你可以用 PHP 进行(某种形式的)函数式编程,这要感谢那些发现了其他语言中的函数式编程元素(比如闭包)并把它们带到 PHP 中来为过程模型和面向对象模型增添趣味的人。
所有这些都意味着你可以在 PHP 中挑选你的编程范式,而 PHP 很少阻止你将它们混合在一起。本书远非函数式编程的啦啦队长,而是旨在强调每种范式的优点和缺点。为此,“进一步阅读”一节将更详细地介绍每种方法的缺点。
进一步阅读
- PHP 手册中的 PHP 历史
- 面向过程编程的局限性
- “所有的证据都指向 OOP 是扯淡”约翰·巴克
- "函数式编程有哪些限制/缺点?"吉洪·杰尔维斯的回应
PHP 不是函数式语言
PHP 不是函数式语言。让我再说一遍,以防不清楚:PHP 不是函数式语言。当然,你可以用 PHP 进行函数式编程。如果你不能,这将是一本非常短的书。但是 PHP 不是函数式语言。PHP 是一种非常通用的、几乎与范式无关的通用编程语言。但是它仍然不是一种函数式语言。
您可以使用本书中介绍的功能代码来编写整个程序、应用或系统。但是如果你这样做了,你将会错过以下几点:
- 访问用 OO 或过程 PHP 编写的库和其他第三方代码
- 访问您自己现有的不起作用的 PHP 代码
- 对 I/O 的自由访问(PHP 让这变得如此简单)
- 来自你周围其他程序员的支持和理解(除非他们也热衷于函数式编程!)
为了更好地理解 I/O,您可以使用 IO 或 Writer monad 以函数方式处理 I/O,但这通常意味着将脚本执行推到脚本的末尾,虽然函数脚本通常更容易推理和理解,但大多数人发现 monad 的代码流正好相反。对于第三方库,您可能会发现它们有您在函数式风格中使用的很好的函数或对象方法,但是(除非您仔细检查并可能重写它们的代码),您不能确保这些函数的实现遵守了函数原则,如不变性和引用透明性。
我的建议?按照你认为合适的方式混合搭配范例。函数式编程只是你工具箱中的另一个工具;在它看起来能为手头的问题提供解决方案的地方和时间使用它。也就是说,不要走极端!保持事情简单明了的最好方法是将您的代码分成功能性和非功能性代码块。一个显而易见的安排是将你的业务逻辑、高性能算法等写在功能代码中,将它们夹在不纯的面向对象或过程代码之间来处理输入和输出,如图 7-1 所示。
图 7-1。
Example of mixed architecture
有了这样的安排,您就有了清晰的功能代码块来进行推理和测试,并且当问题确实发生时,更容易找出它们可能位于何处。当你面临使用函数式技术更新现有的代码库时,一个好的方法是首先识别出恰好适合图 7-1 中间的代码部分,并优先考虑它们。
这在理论上是好的,但是在现实世界中事情经常变得更加混乱,你的代码范例开始重叠。在接下来的几节中,您将看到在将面向对象和过程性代码与纯函数性代码混合时的一些潜在问题。
Aside
不要在 PHP 中使用单子。真的,PHP 里不要用单子。单子是由纯函数式代码推广的,这些代码没有其他方法来处理像 I/O 这样可能不纯的操作。PHP 不是纯函数式语言,还有很多其他方法可以最小化 I/O 变坏的有害影响。它们可能看起来像一个巧妙的技巧,当你最终“得到”单子时的幸福感是美好的,当你的代码最终通过测试并且正确的值从你的脚本中慢慢流出时的多巴胺热潮是值得珍惜的。有时候你会意外地写出类似单子的代码,或者恰好符合单子定律的代码。只要那种风格是你正常编码风格的一部分,或者是一种必要代码结构的偶然特征,那就可以了。但是,在代码更改时为了单子而保持单子是一件令人头痛的事情,初级开发人员会看着你的代码开始哭泣,而你的半懂技术的老板会看着你的代码说,“回到纯 OOP 吧,大家。”把它们当作玩具,让同性或异性成员惊叹的东西,用于学术研究,或者吓唬实习生,但是看在上帝的份上,不要让它们出现在生产代码中。我知道我已经向你们介绍了它们,向你们展示了它们是如何工作的,并且慷慨地提到了它们。把这想象成一个家长和他们的孩子谈论毒品:你需要确保你的孩子了解他们,从你而不是他们的“朋友”那里获得关于他们的事实,并理解他们。但是你不想让他们用。
总结一下,不要在 PHP 中使用单子。
对象和可变性
当您将一个值传递给一个函数时,您不希望该函数改变原始值。如果是这样的话,它将会使任何依赖于该值的代码变得更加难以推理。当你传递对象时,这说起来容易做起来难。
PHP 中的对象实际上并不存储在变量中。当你使用类似于$my_object = new some_class();
的代码创建一个新对象时,这个对象实际上并没有被赋给变量。相反,对象标识符被分配给变量,这允许访问代码来定位实际的对象。这意味着,如果你将一个对象变量作为参数传递给一个函数,你传递的值是对象标识符,而不是对象本身。当函数内部的代码使用该标识符时,它是在处理原始对象,而不是副本(标识符是一个副本,但却是指向原始对象的忠实副本)。让我们来看一个例子(参见清单 7-1 和清单 7-2 )。
<?php
# Create a class to encapsulate a value
class my_class
{
# The value we want to encapsulate
private $value = 0;
# Constructor to set the value (or default to -1)
public function __construct($initial_value = -1) {
$this->value = $initial_value;
}
# Method to get the value
public function get_value() {
return $this->value;
}
# Method to set the value
public function set_value($new_value) {
$this->value = $new_value;
}
}
# Let's create a new object with a value of 20
$my_object = new my_class(20);
# Check the value
var_dump ($my_object->get_value()); # int(20)
# Demonstrate we can mutate the value to 30
$my_object->set_value(30);
var_dump ($my_object->get_value()); # int (30)
# Now let's create a function which doubles the value
# of the object. Note that the function parameter
# doesn't have a "&" to indicate it's passed by reference
function double_object ($an_object) {
# Get the value from $an_object, double it and set it back
$an_object->set_value( $an_object->get_value() * 2 );
# return the object
return $an_object;
}
# Now we call the function on our $my_object object from
# above, and assign the returned object to a new variable
$new_object = double_object($my_object);
# Check that the returned object has double the value (30)
# of the object we passed in as a parameter
var_dump( $new_object->get_value() ); # int(60)
# Let's just check the value on the original object
var_dump( $my_object->get_value()); # int(60)
# It's also changed. Let's var_dump the original object
# and returned object, and check their object reference number
# (look for the number after the #)
var_dump ($my_object); # #1
var_dump ($new_object); # #1
# They're both the same. Just for clarity, create a new
# object from scratch and check it's reference number
$last_object = new my_class();
var_dump ($last_object); # #2
Listing 7-1.passing_objects.php
int(20)
int(30)
int(60)
int(60)
object(my_class)#1 (1) {
["value":"my_class":private]=>
int(60)
}
object(my_class)#1 (1) {
["value":"my_class":private]=>
int(60)
}
object(my_class)#2 (1) {
["value":"my_class":private]=>
int(-1)
}
Listing 7-2.passing_objects-output.txt
那么,当你想在一些函数代码中将对象作为参数传递时,你该怎么做呢?有几种选择。第一个很简单:不要做。你真的需要传递整个对象吗?在许多情况下,您可以从对象中获取一个(或两个)值,将其传递到一个组合的函数堆栈中(可以包括您正在使用的类中的方法),然后将函数代码的结果设置回您的对象中。这确保了您的对象在代码的功能部分不会发生变化。清单 7-3 显示了一个简单的例子。
<?php
# use our trusty compose function
include('../Chapter 3/compose.php');
# The same class as before, but with an added static method
class new_class
{
private $value = 0;
public function __construct($initial_value = -1) {
$this->value = $initial_value;
}
public function get_value() {
return $this->value;
}
public function set_value($new_value) {
$this->value = $new_value;
}
# a static method to halve the provided value
public static function halve($value) {
return $value / 2;
}
}
# Let's create a new object with an initial value of 25
$my_object = new new_class(73.4);
# Let's stack some math functions together including our
# static method above
$do_math = compose (
'acosh',
'new_class::halve',
'floor'
);
# Now let's actually do the math. We set the object value
# to the result of $do_math being called on the original value.
$my_object->set_value(
$do_math(
$my_object->get_value()
)
);
# Show that our object value has been changed. Note that nothing changed
# while we were in our functional (compose) code.
var_dump ( $my_object->get_value() ); # float(2)
Listing 7-3.
static_methods.php
如果您的对象包含多个函数需要处理的值,您当然可以首先将它们提取到一个数据结构中,比如一个数组。
如果您确实需要将整个对象传递给函数,那么您需要首先克隆它,因为没有直接的方法将对象的内容通过值传递给函数。您将需要确保代码中没有其他内容试图访问这些克隆的对象,并且每个返回对象的函数都需要在出于完全相同的原因返回之前克隆它。清单 7-4 展示了这个过程的机制,输出如清单 7-5 所示。
<?php
# use our trusty compose function
include('../Chapter 3/compose.php');
# The same class as previously
class my_class
{
private $value = 0;
public function __construct($initial_value = -1) {
$this->value = $initial_value;
}
public function get_value() {
return $this->value;
}
public function set_value($new_value) {
$this->value = $new_value;
}
}
# A function to triple the value of the object
$triple_object = function ($an_object) {
# First clone it to make sure we don't mutate the object that
# $an_object refers to
$cloned_object = clone $an_object;
# Then set the value to triple the current value
$cloned_object->set_value( $cloned_object->get_value() * 3 );
# and return the new object
return $cloned_object;
};
# A function to multiply the value of the object by Pi.
# Again we clone the object first and return the mutated clone
$multiply_object_by_pi = function ($an_object) {
$cloned_object = clone $an_object;
$cloned_object->set_value( $cloned_object->get_value() * pi() );
return $cloned_object;
};
# Let's create an object encapsulating the value 10.
$my_object = new my_class(10);
# We'll compose the above functions together
$more_math = compose(
$triple_object,
$multiply_object_by_pi,
$triple_object
);
# and then call that composition on our object.
var_dump ( $more_math($my_object) );
# Let's check our original object remains unchanged
var_dump ($my_object);
Listing 7-4.
clones.php
object(my_class)#4 (1) {
["value":"my_class":private]=>
float(282.74333882308)
}
object(my_class)#3 (1) {
["value":"my_class":private]=>
int(10)
}
Listing 7-5.clones-output.txt
对象#1 和#2 是包含$triple_object and $multiply_object_by_pi
函数的闭包对象。对象#3 是原始对象,对象#4 是返回的对象。每个函数中的克隆对象只有在变量$cloned_object
引用它们时才存在。函数一返回,$cloned_object
(像函数范围内的所有变量一样)就被销毁,PHP 自动删除不再被引用的对象。因此,通过调用$more_math
函数创建的对象可以使用#4 标识符(尽管它在var_dump
语句后也会被销毁,因为它没有被赋给任何变量)。
正如您从前面的代码中看到的,这样做可能会很麻烦,并且您正在进行的克隆类型是一个“浅层”拷贝,因此根据您的类/对象的结构,会有一些限制。如果您已经为其他目的实现了一个__clone
方法,请注意,它也会在这些情况下自动使用。如果对象中的方法访问另一个对象,克隆它不会克隆另一个对象。出于这些原因,我建议您将函数代码与对象分开,至少在涉及值传递时是这样。
最后,值得一提的是 PHP 资源变量(比如文件指针和数据库句柄)和对象一样,只是对外部资源的引用。与对象一样,这意味着当您将它们作为参数传递给函数时,实际的资源没有被复制,只有变量值。您已经了解了为什么像文件这样的资源被认为是外部状态,并且会给函数带来副作用。但是,如果您认为某个特定的资源对于您的特定目的(例如,用于日志记录的输出文件)是足够稳定和有保证的,那么一定要考虑到所指向的资源可能会被脚本的另一部分所改变,因为您的变量指向的是共享资源,而不是您自己的独特副本。
进一步阅读
- PHP 手册中的对象和引用,概述了对象变量和对象本身之间的关系
- PHP 手册中的对象克隆,详细说明了通过
clone
关键字克隆对象的级别
带有对象的不可变数据
已经了解了为什么对象在某种意义上是天生可变的,现在让我们看看硬币的另一面,看看对象如何帮助您保持其他数据结构不可变。
正如我在本书前面所讨论的,PHP 拥有的唯一真正不可变的构造是常量,使用define
或const
声明。不过,也有一些缺点。只有标量和最近的数组可以被定义为常量;对象不能。常量也是全局定义的(像超全局变量一样),而不是在普通的变量范围内定义的,我之前说过要避免这种情况,因为它被认为是无法推理的外部状态(因此是函数的副作用)。在这种情况下,这似乎不是什么大问题,因为每个常数一旦定义就不可改变,所以你可以依赖它的值。然而,考虑一下define
语句允许您有条件地创建常数,并根据变量或其他计算值设置它们的值,而不是硬编码的值。这意味着你根本不能依赖有值的常量,更不用说期望值了,所以如果它不是作为函数的参数之一传入的,你就不能可靠地推断出函数的输出。
解决这些问题的一种方法是创建一个类来封装值并强制保持它们不变(或不变异)。这种类的对象可以根据给定的值构造,然后通过避免使用 setters 之类的方法来确保该值在对象的生命周期内不会改变。作为一个普通的对象变量,通常的变量范围规则将适用。前一节中提到的关于传递对象的问题在这里不太适用,因为您有意地最小化了对象的可变性。
因此,让我们来看看创建一个常量数组作为不可变对象的方法(参见清单 7-6 和清单 7-7 )。
<?php
# Create a class to represent an immutable array
# Make the class "final" so that it can't be extended to add
# methods to mutate our array
final class const_array {
# Our array property, we use a private property to prevent
# outside access
private $stored_array;
# Our constructor is the one and only place that we set the value
# of our array. We'll use a type hint here to make sure that we're
# getting an array, as it's the only "way in" to set/change the
# data, our other methods can be sure they are then only dealing
# with an array type
public function __construct(array $an_array) {
# PHP allows us to call the __construct method of an already created
# object whenever we want as if it was a normal method. We
# don't want this, as it would allow our array to be over written
# with a new one, so we'll throw an exception if it occurs
if (isset($this->stored_array)) {
throw new BadMethodCallException(
'Constructor called on already created object'
);
};
# And finally store the array passed in as our immutable array.
$this->stored_array = $an_array;
}
# A function to get the array
public function get_array() {
return $this->stored_array;
}
# We don't want people to be able to set additional properties on this
# object, as it de facto mutates it by doing so. So we'll throw an
# exception if they try to
public function __set($key,$val) {
throw new BadMethodCallException(
'Attempted to set a new property on immutable class.'
);
}
# Likewise, we don't want people to be able to unset properties, so
# we'll do the same again. As it happens, we don't have any public
# properties, and the methods above stop the user adding any, so
# it's redundant in this case, but here for completeness.
public function __unset($key) {
throw new BadMethodCallException(
'Attempted to unset a property on immutable object.'
);
}
}
# Let's create a normal array
$mutable_array = ["country" => "UK", "currency" => "GBP", "symbol" => "£"];
# and create an const_array object from it
$immutable_array = new const_array($mutable_array);
var_dump ($immutable_array);
# Let's mutate our original array
$mutable_array["currency"] = "EURO";
# our const_array is unaffected
var_dump ($immutable_array);
# We can read the array values like normal
foreach ( $immutable_array->get_array() as $key => $value) {
echo "Key [$key] is set to value [$value] \n\n";
};
# And use dereferencing to get individual elements
echo "The currency symbol is ". $immutable_array->get_array()["symbol"]."\n\n";
# Need to copy it? Just clone it like any other object, and the methods
# which make it immutable will be cloned too.
$new_array = clone $immutable_array;
var_dump ($new_array);
# The following operations aren't permitted though, and will throw exceptions
# $immutable_array->stored_array = [1,2,3];
# BadMethodCallException: Attempted to set a new property on immutable class
# $immutable_array->__construct([1,2,3]);
# BadMethodCallException: Constructor called on already created object
# unset($immutable_array->get_array);
# BadMethodCallException: Attempted to unset a property on immutable object.
# $immutable_array->new_prop = [1,2,3];
# BadMethodCallException: Attempted to set a new property on immutable class
# $test = new const_array();
# TypeError: Argument 1 passed to const_array::__construct()
# must be of the type array, none given
# class my_mutable_array extends const_array {
#
# function set_array ($new_array) {
#
# $this->stored_array = $new_array;
#
# }
#
# };
# Fatal error: Class my_mutable_array may not inherit from final
# class (const_array)
# Unfortunately, there is no practical way to stop us overwriting the object
# completely, either by unset()ing it or by assigning a new value to the
# object variable, such as by creating a new const_array on it
$immutable_array = new const_array([1,2,3]);
var_dump($immutable_array); # new values stored
Listing 7-6.const_array.php
object(const_array)#1 (1) {
["stored_array":"const_array":private]=>
array(3) {
["country"]=>
string(2) "UK"
["currency"]=>
string(3) "GBP"
["symbol"]=>
string(2) "£"
}
}
object(const_array)#1 (1) {
["stored_array":"const_array":private]=>
array(3) {
["country"]=>
string(2) "UK"
["currency"]=>
string(3) "GBP"
["symbol"]=>
string(2) "£"
}
}
Key [country] is set to value [UK]
Key [currency] is set to value [GBP]
Key [symbol] is set to value [£]
The currency symbol is £
object(const_array)#2 (1) {
["stored_array":"const_array":private]=>
array(3) {
["country"]=>
string(2) "UK"
["currency"]=>
string(3) "GBP"
["symbol"]=>
string(2) "£"
}
}
object(const_array)#3 (1) {
["stored_array":"const_array":private]=>
array(3) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(3)
}
}
Listing 7-7.const_array-output.txt
你可以在最后看到它并不是完全不可改变的。您可以用新的值或对象完全覆盖对象变量。您可以向该类添加一个__destruct
方法,如果对象被破坏(通过取消设置或覆盖),该方法将抛出一个异常,但是这有两个问题。首先,当你的脚本终止时,所有的对象都被销毁了,所以每次脚本运行时都会抛出异常,如果你在其他对象上有其他的析构函数而没有被调用,这可能会是一个问题。第二,正如我前面描述的,对象变量只是对实际对象的引用。这意味着如果你创建一个不可变的对象作为$a
,然后做$b = $a
,最后做unset($a)
,你的__destruct
方法不会触发,因为实际的对象仍然存在,因为它被$b
引用。出于这些原因,在大多数情况下,在__destruct
上抛出异常可能并不实用。尽管如此,创建像这样的基本不可变的对象是防止大多数类型的数据结构(包括对象本身)中值的意外变化的一种有用的方法。
作为外部状态的对象属性
对象方法是函数,这很好,你可以在你的函数组合中使用它们。对象也有封装到对象中的属性(值)。封装作为一个概念是好的;您已经看到了它在闭包中的作用,在闭包中,您通过函数上的use
子句将值封装到您的作用域中。但是一个对象属性并不等同于一个被拉入闭包函数的值;它只在对象内部,而不是在单个方法内部。当然,私有属性只能由对象中的方法访问,但是对象中的任何方法(私有或公共)都可以访问和更改该属性。实际上,该属性位于类中的“全局”类型范围内,至少就方法而言是如此,因此您应该能够理解为什么在函数式编程中使用它们时会出现问题。下面的例子演示了一个属性如何有效地变成外部状态,当你没有明确地把它作为一个参数传递给你的函数(方法)调用时,你不能总是推断出函数对于一个特定的输入会给出什么样的输出。参见清单 7-8 和清单 7-9 。
<?php
# Get our compose function
require '../Chapter 3/compose.php';
# This class will provide a set of methods to work with tax
class tax_functions {
# Store the rate of tax
private $tax_rate;
# Our constructor sets the tax rate initially
public function __construct($rate) {
$this->tax_rate = $rate;
}
# Provide a method to set the tax rate at any point
public function set_rate($rate) {
$this->tax_rate = $rate;
}
# A method to add tax at the $tax_rate to the $amount
public function add_tax($amount) {
return $amount * (1 + $this->tax_rate / 100);
}
# A method to round the $amount down to the nearest penny
public function round_to($amount) {
return floor($amount * 100) / 100;
}
# A function to format the $amount for display
public function display_price($amount) {
return '£'.$amount.' inc '.$this->tax_rate.'% tax';
}
}
# So let's create an object for our program containing the
# methods, with the tax rate set at 10%
$funcs = new tax_functions(10);
# Now let's compose our methods into a flow that adds tax, rounds
# the figure and then formats it for display.
# Note that to pass a method of an object as a callable, you need
# to give an array of the object and method name. If you are using
# static class methods, you can use the class::method notation instead
$add_ten_percent = compose (
[$funcs, 'add_tax'],
[$funcs, 'round_to'],
[$funcs, 'display_price']
);
# We've composed our $add_ten_percent function, but we may not want to use it
# until much later in our script.
# In the mean-time, another programmer inserts the following line in our
# code in between...
$funcs->set_rate(-20);
# and then we try to use our $add_ten_percent function to add
# tax to 19.99, hopefully getting the answer £21.98 inc 10% tax
var_dump( $add_ten_percent(19.99) ); # £15.99 inc -20% tax
Listing 7-8.properties.php
string(20) "£15.99 inc -20% tax"
Listing 7-9.properties-output.txt
如您所见,对象属性可以被视为函数的外部状态。这个例子中的副作用是由某人改变属性值引起的(尽管在这个非常做作和明显的例子中!)在你的函数流之外给了你一个你没有预料到的结果。
那么,你对此能做些什么呢?有几种策略。
- 不要用属性!将值包装到返回值的函数/方法中。
- 尽可能使用
const
将属性声明为常量。 - 使用静态类方法,而不是实例化的对象;那么就没有需要担心的属性或
$this
。
简而言之,像对待函数外部的任何其他状态一样对待对象属性。
在线杂质
在前面,您看到了如何构建您的程序,以便将有问题的不纯代码段与功能代码段分开。您还在本书的前面看到了单子,它允许您巧妙地分离这些杂质(通常将执行推到代码定义的末尾)。从实用的角度来看,这两种方法都有问题。单子的编写和使用都很复杂。结构化代码将 I/O 从功能代码中分离出来可能是不可取的,例如,当您需要记录数据或更新用户在长时间运行的脚本中的进度时。
一个可能的解决方案(这会让函数纯粹主义者尖叫)是传递型函数。您可以将这些函数组合到正常的函数链中,这些函数执行以下操作:
- 将前一个函数的返回值作为其参数
- 做一些不纯洁的行为(例如,记录)
- 返回原始参数(未变异的)作为其返回值
这里的关键是函数不仅仅是引用透明的,而且对函数链完全透明。因此,它可以在任何时候在函数链中的任何地方被添加或删除,而不会影响链的输出。
清单 7-10 展示了一个随走随记的例子,使用一个名为$transparent
的包装函数来创建一个透明版本的不纯日志记录函数。清单 7-11 显示了输出。
<?php
# Grab our compose function
require('../Chapter 3/compose.php');
# Define some math functions
$add_two = function ( $a ) {
return $a + 2;
};
$triple = function ( $a ) {
return $a * 3;
};
# Now we're going to create a "dirty" function to do some logging.
$log_value = function ( $value ) {
# Do our impure stuff.
echo "Impure Logging : $value\n";
# Oops, we mutated the parameter value...
$value = $value * 234;
# ...and returned it even more mutated
return $value.' is a number';
};
# Now we're going to create a higher-order function which returns a
# wrapped function which executes our impure function but returns
# the original input parameter rather than any output from our impure
# function. Note that we must pass $value to $impure_func by value and
# not by reference (&) to ensure it doesn't mess with it. Also see
# the sections on the mutability of objects if you pass those through,
# as the same concerns will apply here.
$transparent = function ($impure_func) {
return function ($value) use ($impure_func) {
$impure_func($value);
return $value;
};
};
# Compose the math functions together, with the $log_value impure function
# made transparent by our wrapper function
$do_sums = compose(
$add_two,
$transparent($log_value),
$triple,
$transparent($log_value)
);
# We should get the expected result
var_dump( $do_sums(5) ); # 21
Listing 7-10.transparent.php
Impure Logging : 7
Impure Logging : 21
int(21)
Listing 7-11.transparent-output.txt
需要注意的一个关键点是,$transparent
包装函数并没有使$log_value
函数变得纯粹。不纯函数仍然可以通过抛出异常或错误来影响代码,虽然您可以推理您的函数代码,但在很大程度上忽略不纯函数,您不能(必然地)推理不纯函数本身。然而,它是一个有用的工具,可以最小化不纯函数的潜在影响,出于实用的原因,您希望在代码中包含不纯函数。它最适合执行输出,因为通常执行输入的主要原因是获取要在函数中处理的值,而这种方法不允许。图 7-2 展示了这样一个透明的功能是如何适应混合功能和非功能代码的正常流程的。
图 7-2。
Mixed architecture with transparent functions
过程编程注意事项
混合函数式编程和过程式编程通常问题较少;毕竟,您在函数式编程中使用的大多数语法和结构都是从标准过程代码中借用的。正如本章前面所描述的,尽可能将两者分开是值得的;然而,随着您对对象可变性关注的减少,常见的是将程序代码块包装在函数中,而不是将其装订起来。如果你这样做了,只需注意你在函数外部的状态上做了什么,尽量不要让它影响你的函数的返回值。最常见的情况是,当“有必要”(理解为实用)从函数流中输出或写入文件时,将过程代码包装到函数中。最后,永远记住,仅仅因为你在过程代码中使用了函数,并不一定意味着你在编写函数代码(或者变异函数,比如array_walk
)。
当您需要混合现有的过程代码时,您可以包含或要求脚本,并将它们视为一种功能。当您包含或需要一个文件时,PHP 在当前范围(例如,全局或函数范围)内执行该脚本。如果在脚本中添加返回值语句,PHP 将把它作为include
或require
语句的返回值返回。这意味着,如果你小心的话,你可以把一个程序代码文件包装到一个函数的范围内。考虑清单 7-12 中所示的程序代码。
<?php
# This is some typical procedural code
echo ("a is $a\n");
$number = $a + 5;
$number = $number * 2;
for ($i = 0; $i < 5; $i++) {
echo "We're doing procedural stuff here\n";
};
$b = 50;
# Note the addition of a return statement.
return $number;
Listing 7-12.procedural.php
现在考虑清单 7-13 ,它拉进过程文件两次,但方式略有不同。清单 7-14 显示了输出。
<?php
# First set some variables in global scope
$a = 25;
$b = 0;
# Do a simple require of the file.
$return_value = require "procedural.php";
var_dump ( $return_value ); #60 - the script operated on our $a value of 25
var_dump ( $a ); # 25
var_dump ( $b ); # 50 - the script has mutated $b in the global scope
# Reset $b
$b = 0;
# This function executes the file as if it were a function, within the
# scope of the function. You can pass in a set of parameters as an array,
# and the extract line creates variables in the function scope which
# the code in the file can access. Finally, it requires the file and
# returns the files return value as its own.
$file_as_func = function ($filename, $params) {
extract ($params);
return require $filename;
};
# We'll call it on our procedural.php file, with a couple of parameters
# that have the same name but different values to our global $a and $b
var_dump ( $file_as_func( 'procedural.php', ['a'=>50, 'b'=>100] ) ); # 110
# this clearly operated on our parameter "a" and not the global $a
var_dump ( $a ); # 25
var_dump ( $b ); # 0 - unchanged this time
Listing 7-13.procedural2.php
a is 25
We're doing procedural stuff here
We're doing procedural stuff here
We're doing procedural stuff here
We're doing procedural stuff here
We're doing procedural stuff here
int(60)
int(25)
int(50)
a is 50
We're doing procedural stuff here
We're doing procedural stuff here
We're doing procedural stuff here
We're doing procedural stuff here
We're doing procedural stuff here
int(110)
int(25)
int(0)
Listing 7-14.procedural2-output.txt
正如您所看到的,这个方法为您提供了一种方便的方法来限制一段过程代码的范围,但仍然以参数化的方式推入数据并返回一个返回值。这不会特别增加您对过程代码进行推理的能力,或者限制许多类型的副作用的范围,但是它确实最小化了某些类型的错误的机会,并且帮助您在思想上划分代码。
摘要
您已经看到了混合编码范例时的各种方法和陷阱,但也探索了为什么在应用中为功能代码找到一个位置通常是一个好主意,而不是试图构造完全功能的代码库。PHP 是一门务实的语言,做一个务实的 PHP 程序员吧!
八、在应用中使用助手库
到目前为止,在本书中,您已经看到了如何从头开始编写自己的函数代码(尽管您在 monad 部分使用了一些库)。在这一章中,你将更深入地了解一些更流行的库,它们可以帮助你把你的应用变成功能强大的应用。这些可以帮助您减少开发时间,实现复杂或复杂的功能,或者将您的代码整理成一致的风格。
如何选择库
不幸的是,PHP 中没有单一的函数库可以推荐。正如您已经发现的,与其他一些语言不同,PHP 没有关于函数式编程以及如何构造函数式编程的正式思想,所以我将要介绍的函数库包含了实现函数式结构和过程的不同方式。您在自己的项目中选择使用哪一个将取决于您自己的编码风格、您的 PHP 经验的一般水平,并且在某些情况下,哪些库提供了您想要使用的特定构造。当您依次查看每个库时,我将对它的任何特殊优势或劣势进行评论;您将看到一些示例代码;我将列出该库的官方下载、网站和文档。在选定一个库之前,您可能希望了解每个库的以下方面:
- 这个库包含你需要的所有功能吗?
- 库还在维护吗?(如果它与当前的 PHP 版本兼容并且稳定,这可能没什么关系。)
- 你需要的功能在这个库中实现得好吗(一致的,高性能的,可扩展的)?
- 自动循环等功能是否存在(如果您需要的话)?
- 库到底有多“纯”?
最后一项值得进一步解释。许多可用的库对不纯的、产生副作用的操作采取了实用的方法,比如我一直提倡的文件 I/O,并简单地将它们划分成单独的函数。其他人严格执行无副作用的代码,并实现他们自己的严格类型和不可变值。确保你对你的库强加的(或不强加的)功能纯度水平感到满意,然后再围绕它构建你的应用。
挑选库
为一个函数使用整个库通常不太理想,因为包括整个代码库只是为了使用其中的一小部分而浪费资源。函数式编程库,如果它们是以本书中提到的函数方式构建的,通常是由小的、单一用途的、可组合的函数组成的。假设你很高兴这个库处于一个稳定的发布点,深入源代码,取出你需要的函数,并粘贴/重新实现到你的代码中(当然,要尊重版权!).也就是说,如果您希望您的项目能够使用更多的功能,或者库还不稳定,那么包含(和更新)库的开销可能是值得的。
基于 Ramda 的库
Ramda 是一个用于函数式编程的流行 JavaScript 库。下面的 PHP 库受到了 Ramda 库的特性和语法的启发。
普拉达
- 下载:
https://github.com/kapolos/pramda
- 文档:
https://github.com/kapolos/pramda#pramda
- 要点:Pramda 具有广泛的惰性评估和自动生成功能。该文档对使用该库的函数式编程概念进行了很好的基本介绍,但对所提供的函数的文档却很有限。
- 示例代码:在清单 8-1 中,您将重复您的 Shakespeare analysis 函数来获取三个包含单词 hero 的长句。注意,库函数是作为
P
类的静态方法公开的(例如P::compose
)。要使用该库,只需需要它或通过 Composer 安装它并需要自动加载程序。清单 8-2 显示了输出。
<?php
# Require the library
require('pramda/src/pramda.php');
# Start timing
$start_time = microtime(true);
# The same $match_word function
$match_word = function($word, $str) {
return preg_match("/[^a-z]${word}[^a-z]/i", $str);
};
# we'll replace str_len with a composition of P::size and str_split.
# it provides no advantage here, other than to demostrate the composition
# of functions (and the fact that there's more than one way to skin a cat).
# Note that Pramda's compose method operates "right to left", i.e. it
# executes functions in the opposite order to the compose function
# we've used up to this point. Also note that we call the composed function
# immediately upon creation on the $str.
$longer_than = function($len, $str) {
return P::compose(
'P::size',
'str_split'
)($str) > $len;
};
# Create a function to get lines with the word hero in. Pramda doesn't have
# a simple partial function, so instead we curry the function.
$match_hero = P::curry2($match_word)('hero');
# Ditto with a function to get lines with more than 60 chars
$over_sixty = P::curry2($longer_than)(60);
# Pramda's own functions are mostly auto-currying (where it make sense),
# and so we can simply call the P::filter method (similar to array_filter)
# with just a callback, which creates a partial/curried function that
# just needs an array to be called with. We don't need to explicitly
# call a currying function on it.
$filter_hero = P::filter($match_hero);
$filter_sixty = P::filter($over_sixty);
$first_three = P::slice(0,3);
# Now we'll compose these together. Note that we use P::pipe and not P::compose,
# as mentioned above P::compose operates right-to-left, whereas it's easier
# to read left-to-right (or top-to-bottom as we've laid the code out here).
# If you look at the Pramda source code, P::pipe simply reverses the arguments
# and calls P::compose on them!
$three_long_heros = P::pipe(
'P::file', //lazy file reader
$filter_hero,
$filter_sixty,
$first_three
);
# We call the composed function in the normal way
$result = $three_long_heros('../Chapter 5/all_shakespeare.txt');
print_r($result);
echo 'Time taken : '.(microtime(true) - $start_time);
Listing 8-1.pramda-example.php
Array
(
[0] => Enter DON PEDRO, DON JOHN, LEONATO, FRIAR FRANCIS, CLAUDIO, BENEDICK, HERO, BEATRICE, and Attendants
[1] => Sweet Hero! She is wronged, she is slandered, she is undone.
[2] => Think you in your soul the Count Claudio hath wronged Hero?
)
Time taken : 1.4434311389923
Listing 8-2.pramda-example-output.txt
如你所见,输出的句子和你在第五章得到的是一样的。在清单 8-3 中,您将在一家餐馆的菜单上做一些工作(因为我又饿了)以查看更多可用于数组的函数。清单 8-4 显示了输出。
<?php
# Get the library
require('pramda/src/pramda.php');
# Define our menu data
$menu = [
[ 'Item' => 'Apple Pie',
'Category' => 'Dessert',
'Price' => 4.99,
'Ingredients' => ['Apples' => 3, 'Pastry' => 1, 'Magic' => 100]
],
[ 'Item' => 'Strawberry Ice Cream',
'Category' => 'Dessert',
'Price' => 2.22,
'Ingredients' => ['Strawberries' => 20, 'Milk' => 10, 'Sugar' => 200]
],
[ 'Item' => 'Chocolate and Strawberry Cake',
'Category' => 'Dessert',
'Price' => 5.99,
'Ingredients' => ['Chocolate' => 4, 'Strawberries' => 5, 'Cake' => 4]
],
[ 'Item' => 'Cheese Toasty',
'Category' => 'Main Courses',
'Price' => 3.45,
'Ingredients' => ['Cheese' => 5, 'Bread' => 2, 'Butter' => 6]
]
];
# Let's get a list of all the distinct ingredients used in the menu
$all_ingredients = P::pipe(
# get just the ingredient array from each element
P::pluck('Ingredients'),
# reduce them into a single array
P::reduce('P::merge', []), #
# grab just the keys (the ingredient names)
# which will be unique due to the merge above
'array_keys'
);
var_dump( $all_ingredients($menu) );
# This time we want to count the quantity of fruit used in our menu, if
# we were making one of each dish
$fruit = ['Apples' => true, 'Strawberries' => true, 'Plums' => true];
# A function to get only items who contain fruit
$get_only_fruit = function($item) use ($fruit) {
# P::prop returns an array element with a particular key, in this
# case the element holding an array of Ingredients, from which
# we get the elements which intersect with the keys in $fruit
return array_intersect_key(P::prop('Ingredients', $item), $fruit);
};
$count_fruit = P::pipe ( # compose a function which...
P::map( # ... maps a function onto the input which
P::pipe(
$get_only_fruit, # gets the fruits and
'P::sum' # sums the quantities
) # for each element/item
),
'P::sum' # ...and then sums all the quantities
);
var_dump( $count_fruit($menu) ); #28 (3 apples, 20 strawberries, 5 strawberries)
# Now let's say we want to get a dessert menu, ordered by price,
# starting with the most expensive to increase our profits
$dessert_menu = P::pipe(
# First, sort the data by price
P::sort( P::prop('Price') ),
# Reverse the results so the most expensive is first
'P::reverse',
# Filter the results so that we only have
# desserts
P::filter(
function ($value, $key) {
return P::contains('Dessert', $value);
}
),
# P::filter returns a generator, but because we need
# to iterate over it twice below, we need to convert
# to an array first
'P::toArray',
# Now let's pick out just the information we want to
# display in our menu
function ($items) {
# Get an array of Item names to use as keys,
# and an array of Prices to use as values,
# and array_combine them into a single array.
# Again, P:pluck returns a generator, we want
# an array.
return array_combine(
P::toArray( P::pluck('Item',$items) ),
P::toArray( P::pluck('Price',$items) )
);
}
);
print_r( $dessert_menu($menu) );
Listing 8-3.pramda-example2.php
array(11) {
[0]=>
string(6) "Apples"
[1]=>
string(6) "Pastry"
[2]=>
string(5) "Magic"
[3]=>
string(12) "Strawberries"
[4]=>
string(4) "Milk"
[5]=>
string(5) "Sugar"
[6]=>
string(9) "Chocolate"
[7]=>
string(4) "Cake"
[8]=>
string(6) "Cheese"
[9]=>
string(5) "Bread"
[10]=>
string(6) "Butter"
}
int(28)
Array
(
[Chocolate and Strawberry Cake] => 5.99
[Apple Pie] => 4.99
[Strawberry Ice Cream] => 2.22
)
Listing 8-4.pramda-example2-output.txt
制药公司
- 下载:
https://github.com/mpajunen/phamda
- 文档:
http://phamda.readthedocs.io/en/latest/
- 重点:Phamda 和 Pramda 差不多(除了名字上的单字符区别!).不过,对于许多任务,Phamda 比 Pramda 快,而且它确实提供了一些 Pramda 还没有的附加功能。
- 示例代码:在清单 8-5 中,您将再次使用菜单数据并探索一些 Pramda 没有的功能,例如
ifElse
、not
和evolve
。要使用该库,只需像这里一样要求它或通过 Composer 安装它。清单 8-6 显示了输出。
<?php
# Load via composer, or require the four files below
require('phamda/src/CoreFunctionsTrait.php');
require('phamda/src/Exception/InvalidFunctionCompositionException.php');
require('phamda/src/Collection/Collection.php');
require('phamda/src/Phamda.php');
use Phamda\Phamda as P;
# Same data as before
$menu = [
[ 'Item' => 'Apple Pie',
'Category' => 'Dessert',
'Price' => 4.99,
'Ingredients' => ['Apples' => 3, 'Pastry' => 1, 'Magic' => 100]
],
[ 'Item' => 'Strawberry Ice Cream',
'Category' => 'Dessert',
'Price' => 2.22,
'Ingredients' => ['Strawberries' => 20, 'Milk' => 10, 'Sugar' => 200]
],
[ 'Item' => 'Chocolate and Strawberry Cake',
'Category' => 'Dessert',
'Price' => 5.99,
'Ingredients' => ['Chocolate' => 4, 'Strawberries' => 5, 'Cake' => 4]
],
[ 'Item' => 'Cheese Toasty',
'Category' => 'Main Courses',
'Price' => 3.45,
'Ingredients' => ['Cheese' => 5, 'Bread' => 2, 'Butter' => 6]
]
];
# A function to mark an item as a "special" if it's price is over 5\. The
# Phamda functions we use here are :
# P::ifElse - If the first parameter is true, call the second parameter, else
# call the third
# P::lt - If the first parameter (5) is less than the second, then return true
# Note that due to auto currying the $price will be supplied as the second
# parameter, which is why we use lt rather than gt
# P::concat - add the "Special: " string to the price, (called if P::lt returns
# true)
# P::identity - the identity function returns the value passed in, so if P::lt
# returns false this is called, and the price is returned unchanged.
#
# Note that we add ($price) on the end to execute the function straight away
$specials = function ($price) {
return P::ifElse(P::lt(5), P::concat('Special: '), P::identity())($price);
};
# A function to format the menu item for our menu
$format_item = P::pipe(
# Get just the fields that we want for the menu
P::pick(['Item','Price']),
# "Evolve" those fields, by applying callbacks to them.
# Item is made into uppercase letters, and Price
# is passed through our $specials function above
# to add Special: to any item that costs over 5
P::evolve(['Item'=>'strtoupper', 'Price'=>$specials])
);
# A function to parse our menu data, filter out non-desserts,
# and format the remaining items
$new_dessert_menu = P::pipe(
# It would be more robust to use P::contains('Dessert')
# on the P::prop('Category') lest we introduce
# entrées at a later date, but for now to demonstrate
# P::not and the scope of P::contains, we'll do this:
P::filter( P::not ( P::contains('Main Courses') ) ),
# Map the $format_item function above onto the remaining
# (hopefully only Dessert) items
P::map($format_item)
);
# Finally, create our menu
print_r( $new_dessert_menu( $menu ) );
Listing 8-5.phamda-example.php
Array
(
[0] => Array
(
[Item] => APPLE PIE
[Price] => 4.99
)
[1] => Array
(
[Item] => STRAWBERRY ICE CREAM
[Price] => 2.22
)
[2] => Array
(
[Item] => CHOCOLATE AND STRAWBERRY CAKE
[Price] => Special: 5.99
)
)
Listing 8-6.phamda-example-output.txt
您可能想知道为什么使用not
函数,而不是 PHP 自带的!
(感叹号)操作符。事实上,如果你看看 Phamda 的源代码,你会看到!
被用来实现not
功能。然而,!
就其本身而言,不容易组合,不具备自动循环的特性,并且很容易遗漏本应易读的声明性代码。Phamda 的not
函数只是本机!
操作符的一个包装器,将它变成一个一级函数。
基于下划线的库
下划线是一个流行的 JavaScript 函数式编程库,一些库已经尝试将其移植到 PHP,或者受到其语法和特性的启发。注意这里有两个同名的库,所以我将它们标记为(1)和(2)。
Underscore.php(1)
- 下载:
https://github.com/brianhaveri/Underscore.php
- 文档:
http://brianhaveri.github.io/Underscore.php/
- 要点:
Underscore.php
(1)是 JavaScript 库的一个相当直接的端口。虽然功能完善,但在撰写本文时,它已经有六年没有更新了。 - 示例代码:要使用这个库,只需如下所示。在清单 8-7 中,您将使用第五章中的斐波那契示例,使用下划线的
memoize
函数创建一个记忆版本。回到第五章查看这是如何工作的,并将那里的结果与这里的结果进行比较。
<?php
# Get the library
require('Underscore.php/underscore.php');
# The code below is exactly the same as in Chapter 5, except where noted
$fibonacci =
function ($n) use (&$fibonacci) {
usleep(100000);
return ($n < 2) ? $n : $fibonacci($n - 1) + $fibonacci($n - 2);
};
# Here we memoize using the underscore memoize function rather than our own
$memo_fibonacci = __::memoize($fibonacci);
$timer = function($func, $params) {
$start_time = microtime(true);
$results = call_user_func_array($func, $params);
$time_taken = round(microtime(true) - $start_time, 2);
return [ "Param" => implode($params),
"Result" => $results,
"Time" => $time_taken ];
};
print_r( $timer( $fibonacci, [6, '*'] ) );
print_r( $timer( $memo_fibonacci, [6] ) );
print_r( $timer( $fibonacci, [6, '*'] ) );
print_r( $timer( $memo_fibonacci, [6] ) );
print_r( $timer( $memo_fibonacci, [10] ) );
print_r( $timer( $memo_fibonacci, [11] ) );
print_r( $timer( $memo_fibonacci, [8] ) );
# We'll add an extra call with parameter 8
print_r( $timer( $memo_fibonacci, [8] ) );
underscore-memoize.php
Array
(
[Param] => 6*
[Result] => 8
[Time] => 2.5
)
Array
(
[Param] => 6
[Result] => 8
[Time] => 2.5
)
Array
(
[Param] => 6*
[Result] => 8
[Time] => 2.5
)
Array
(
[Param] => 6
[Result] => 8
[Time] => 0
)
Array
(
[Param] => 10
[Result] => 55
[Time] => 17.72
)
Array
(
[Param] => 11
[Result] => 89
[Time] => 28.73
)
Array
(
[Param] => 8
[Result] => 21
[Time] => 6.71
)
Array
(
[Param] => 8
[Result] => 21
[Time] => 0
)
Listing 8-7.underscore-memoize-output.txt
如果你将这些结果与第五章中的结果进行比较,你会注意到一些不同之处。具体来说,当参数完全匹配时,时间只会减少。例如,计算第 11 个数字需要很长时间,即使你已经计算了第 10 个。这个库中的记忆函数只记忆外部的函数调用,而不记忆内部的递归调用。因此,该函数要么花费全部时间运行,要么在第二次调用第 6 个和第 8 个斐波那契数列时花费 0。
这里的教训是要经常检查你所依赖的函数的实现细节,即使它们的名字是一样的。你会发现在不同的库中有许多不同的记忆功能,有许多不同的操作方式。
在清单 8-8 中,您将看到库的节流函数。这是一个创建自节流函数的高阶函数,每 x 毫秒只能成功调用一次。清单 8-9 显示了输出。
<?php
# Get the library
require('Underscore.php/underscore.php');
# Create a simple function to output a dot
$write = function ($text) { echo '.'; };
# create a new function which is a throttled version of
# $write. It will execute at a maximum once per 1000ms.
# Any other calls during that 1000ms will be ignored.
$throttled_write = __::throttle( $write, 1000);
# Let's call $throttled_write 10 million times. On my
# system that takes a little over 7 seconds, but as it will
# only *actually* execute once every 1000ms (1sec) we
# will get a line of 7 dots printed.
__::times(10000000, $throttled_write);
Listing 8-8.underscore-throttle.php
.......
Listing 8-9.underscore-throttle-output.txt
强调
- 下载:
https://github.com/Im0rtality/Underscore
- 文档:
https://github.com/Im0rtality/Underscore/wiki/Intro
- 要点:下划线是 JavaScript 库的一个更新更全面的端口。它提供了内置的函数链,但在文档方面却很少。当你需要更多地了解一个特定的函数时,你需要钻研源代码,尽管它写得相当好并且容易理解。注意,各种变化意味着文档中的例子不起作用(例如,
pick
函数被重命名为pluck
)。 - 示例代码:要使用这个库,您需要通过 Composer 安装它。清单 8-10 展示了如何将函数调用链接在一起。清单 8-11 显示了输出。
<?php
# Autoload the library
require __DIR__ . '/vendor/autoload.php';
use Underscore\Underscore;
# Run a set of chained functions. Note that we're *not* composing
# a function to be run later, but executing the series of functions
# right here.
# Some data to work with
$foods = [ 'Cheese' => 'Cheddar',
'Milk' => 'Whole',
'Apples' => 'Red',
'Grapes' => 'White'
];
# The ::from function "loads" an array of data into the chain
$result = Underscore::from($foods)
# Let's map a function to uppercase each value and prepend the
# array key to it.
->map(function($item,$key) {
return strtoupper($key.' - '.$item);
})
# Invoke invokes a function over each element like map
->invoke(function($item) { var_dump($item);})
# Shuffle the order of the array
->shuffle()
# Finally generate the return value for the function chain which
# is the array returned by shuffle()
->value();
# Output the final array
var_dump($result);
Listing 8-10.underscore-chain.php
string(16) "CHEESE - CHEDDAR"
string(12) "MILK - WHOLE"
string(12) "APPLES - RED"
string(14) "GRAPES - WHITE"
array(4) {
["Grapes"]=>
string(14) "GRAPES - WHITE"
["Cheese"]=>
string(16) "CHEESE - CHEDDAR"
["Apples"]=>
string(12) "APPLES - RED"
["Milk"]=>
string(12) "MILK - WHOLE"
}
Listing 8-11.underscore-chain-output.txt
Underscore.php(2)
- 下载:
https://github.com/Anahkiasen/underscore-php
- 文档:
http://anahkiasen.github.io/underscore-php/
- 要点:Underscore.php(2)可能是你见过的最灵活的下划线克隆库,也是记录最好的。它不是一个直接的端口,而是受 JavaScript 库风格的启发,同时部署了 PHP 所能提供的最好的功能。
- 示例代码:要使用这个库,您需要通过 Composer 安装它。清单 8-12 展示了调用和链接方法的灵活方式,并展示了与原生 PHP 函数的集成。清单 8-13 显示了输出。
<?php
# Autoload the library
require __DIR__ . '/vendor/autoload.php';
# We're going to use the Arrays type
use Underscore\Types\Arrays;
# Some data
$data = [10, 25, 38, 99];
# A helper function, returns true if $number is Equivalent
$is_even = function ($number) {
return $number % 2 == 0;
};
# We can call the library functions as static methods
var_dump( Arrays::average($data) ); # 43
# We can chain them together, here we load our data with from(),
# filter out the odd number (25 & 99) with our $is_even function,
# and then sum the remaining even numbers
var_dump ( Arrays::from($data)
->filter($is_even)
->sum()
); #10+38 = 48
# We can also instantiate an object to encapsulate our data,
# and call the methods directly on that (which is effectively what the
# static methods do in the background.
$array = new Arrays($data);
var_dump( $array->filter($is_even)->sum() ); #48 again
# The following chain contains a "reverse" function. However no such
# function exists in the library. The library will attempt to use
# native PHP functions for such calls, for arrays it tries to find
# a native function with the same name prefixed by array_, so in
# this case it will use the native array_reverse function.
var_dump( Arrays::from($data)->reverse()->obtain() );
Listing 8-12.underscore-flexible.php
float(43)
int(48)
int(48)
array(4) {
[0]=>
int(99)
[1]=>
int(38)
[2]=>
int(25)
[3]=>
int(10)
}
Listing 8-13.underscore-flexible-output.txt
杂项库
下面的库不像你到目前为止看到的那些库那样是克隆的,它们试图自立,以自己的方式做事。
军刀
- 下载:
https://github.com/bluesnowman/fphp-saber
- 文档:
https://github.com/bluesnowman/fphp-saber#saber
- 要点:Saber 试图使用自己的基于对象的类型系统为 PHP 带来强类型和不变性。虽然它在这些目标上取得了成功,但它(在我看来)不容易开发,导致代码补丁难以阅读。具有讽刺意味的是,虽然像这样的强类型的想法之一是使您的代码更容易推理,但背离 PHP 标准变量意味着普通 PHP 程序员更容易误解代码。缺乏完整的文档使问题更加复杂。也就是说,如果你的库所执行的那些特征对你的用例很重要,这是你唯一的选择之一。
- 示例代码:要使用这个库,您需要通过 Composer 安装它。清单 8-14 展示了如何创建强类型值并对它们应用函数链。清单 8-15 显示了输出。
<?php
# Autoload the library
require __DIR__ . '/vendor/autoload.php';
# You will need to use the Saber datatypes for all data
use \Saber\Data\IInt32;
# Ordinary PHP variable
$start = 20;
# To work with the value, we need to "box" it into a Saber object
# which encapsulates it in a "strictly typed" object
$boxed_value = IInt32\Type::box($start);
# We can chain functions onto the boxed value (note that the parameters
# for those functions also need to be the correct boxed types)
$boxed_result = $boxed_value -> increment() # 21
-> increment() # 22
-> multiply( IInt32\Type::box(3) ) # 66
-> decrement(); # 65
# To get the value back out into a standard PHP variable we need to "unbox" it
var_dump( $boxed_result->unbox() ); # 65
# And check that the original boxed value object that we chained the
# functions on is unmutated
var_dump( $boxed_value->unbox() ); # 20
Listing 8-14.sabre-example.php
int(65)
int(20)
Listing 8-15.sabre-example-output.txt
函数式 PHP
- 下载:
https://github.com/lstrojny/functional-php
- 文档:
https://github.com/lstrojny/functional-php/blob/master/docs/functional-php.md
- 要点:函数式 PHP 是一组旨在简化函数式编程的函数原语。它得到了相对良好的维护,尽管下划线被列为其灵感之一,但它也借鉴了 Scala、Dojo(一个 JavaScript 工具包)和其他资源。它有很好的文档,GitHub 主页甚至有一幅关于尾部递归的 XKCD 漫画,有什么不喜欢的呢?嗯,虽然它在函数原语(函数如
map
、pluck
、sort
等)上很强。),它在功能性构图方面的特征赋予得不太好。 - 示例代码:要使用这个库,您需要通过 Composer 安装它。清单 8-16 展示了菜单数据中使用的一些功能。清单 8-17 显示了输出。
<?php
# Autoload the library
require __DIR__ . '/vendor/autoload.php';
# The recommended way to use the library (only in PHP 5.6+) is to
# import the individual functions as function names so that you
# don't need to qualify them in the code
use function Functional\select;
use function Functional\reject;
use function Functional\contains;
use function Functional\map;
use function Functional\pick;
use function Functional\sort;
use function Functional\drop_last;
use function Functional\select_keys;
# Our trusty menu data
$menu = [
[ 'Item' => 'Apple Pie',
'Category' => 'Dessert',
'Price' => 4.99,
'Ingredients' => ['Apples' => 3, 'Pastry' => 1, 'Magic' => 100]
],
[ 'Item' => 'Strawberry Ice Cream',
'Category' => 'Dessert',
'Price' => 2.22,
'Ingredients' => ['Strawberries' => 20, 'Milk' => 10, 'Sugar' => 200]
],
[ 'Item' => 'Chocolate and Strawberry Cake',
'Category' => 'Dessert',
'Price' => 5.99,
'Ingredients' => ['Chocolate' => 4, 'Strawberries' => 5, 'Cake' => 4]
],
[ 'Item' => 'Cheese Toasty',
'Category' => 'Main Courses',
'Price' => 3.45,
'Ingredients' => ['Cheese' => 5, 'Bread' => 2, 'Butter' => 6]
]
];
# Define a function to check if a food is a dessert, using the contains
# function. Returns true if it's a dessert
$is_dessert = function ($food) {
return contains($food, 'Dessert');
};
# Using the function above, we can apply it in two different ways to our menu
# data using the select and reject functions.
$desserts = select($menu, $is_dessert);
$mains = reject($menu, $is_dessert);
# A helper function using map and pick to return an array of just item names
$list_foods = function ($foods) {
return map($foods, function ($item) {
return pick($item, 'Item');
});
};
# Output the results of the select and reject statements above, using our
# helper function so we don't dump the whole array contents
echo "Desserts:\n";
print_r ( $list_foods( $desserts ) );
echo "Main Courses:\n";
print_r ( $list_foods( $mains ) );
# Our restaurant is struggling, so we want to dump our cheapest dishes.
# First, we need to use the libraries sort function (with a custom callback # function) to sort our $menu based on $price.
$sorted = sort($menu, function($item1,$item2) {
return $item1["Price"] < $item2["Price"];
}, true);
# Now we want to drop any items that cost less than 3\. We use the drop_last
# function to drop the last elements of our sorted array that are >=3
$expensive_items = drop_last($sorted, function ($item) {
return $item["Price"] >= 3;
});
# Let's see what we're left with :s
echo "Expensive Items:\n";
print_r( $list_foods( $expensive_items ) );
# To create our menu, we want to pick out just the Item and Price, so # we'll map the select_keys function against each element to pick those out.
$new_menu = map($expensive_items, function ($item) {
return select_keys($item, ['Item','Price']);
});
echo "New menu:\n";
print_r($new_menu);
Listing 8-16.functionalphp-example.php
Desserts:
Array
(
[0] => Apple Pie
[1] => Strawberry Ice Cream
[2] => Chocolate and Strawberry Cake
)
Main Courses:
Array
(
[3] => Cheese Toasty
)
Expensive Items:
Array
(
[2] => Chocolate and Strawberry Cake
[0] => Apple Pie
[3] => Cheese Toasty
)
New menu:
Array
(
[2] => Array
(
[Item] => Chocolate and Strawberry Cake
[Price] => 5.99
)
[0] => Array
(
[Item] => Apple Pie
[Price] => 4.99
)
[3] => Array
(
[Item] => Cheese Toasty
[Price] => 3.45
)
)
Listing 8-17.functionalphp-example-output.txt
其他库
你在本章中探索的库(可能)是目前 PHP 中最常用的函数式编程库。还有其他的,你会在本书的附录 C 中找到一些供你探索。这些库也都是通用的函数集合,还有其他一些库专注于特定类型的函数或函数概念。同样,你会在附录中找到它们。
九、使用函数式 PHP 处理大数据
大数据是当今的热门话题,它有望治愈癌症,自动化你的工作,最重要的是提供关于网飞的准确电影建议。然而,当阅读大数据时,你很少听到 PHP 被提及。在这一章中,你将会看到到底什么是大数据,以及为什么使用 PHP 处理大数据是完全可能的。当然,鉴于本书的主题,您将了解函数式编程如何成为构建大数据的完美方式——处理程序来帮助您推理和管理处理大规模数据所需的流程。
什么是大数据?
大数据不仅仅是“大量数据”多年来,组织收集的数据量一直呈指数级增长。对于目前能够存储数 TB 数据的普通台式计算机来说,数量本身并不是数据集成为“大数据”的一个很好的限定条件根据一般经验,如果数据集不能在单台机器上存储和处理(至少不能在合理的时间范围内),我倾向于称之为大数据。大数据的另一个共同特点是数据集没有严格定义;它通常包含一个完整的数据范围(通常是可以收集到的关于一个主题的所有可能的数据),并且通常是在知道其分析的最终目标之前收集的。有时,即使数据可以放在一台机器上,如果它是一个真正全面的数据集,涵盖了正在研究的人口的所有可记录的方面,它通常也可以被称为大数据。
大数据领域的数据分析研究通常侧重于在单台机器上无法完成的数据处理方法。发现和分析数据中以前未知的趋势也是大数据研究和使用中的另一个常见主题。
许多组织使用大数据来帮助实现其业务目标。公司使用大数据来识别购买趋势、客户行为、财务模式以及许多其他有趣和有用的指标和信息。研究科学家使用大数据技术来分析由实验产生的越来越大的数据集,以及通过不断增加的人口监测获得的数据集。政治家使用大数据来计算谁最有可能投票给他们,以及为什么,从而使他们能够以不断增加的粒度来定向他们的信息。
一种处理大数据的技术叫做 MapReduce,最初是由 Google 在早期正式提出的。面对如何使用(现在被认为是)功能不足的商用机器在数十亿个网页中计算页面排名(一种显示给定网页连接程度的指标)等信息的问题,谷歌工程师想出了一种方法,将任务分解为可以分布在许多机器上的较小函数,然后将结果合并成一组答案。虽然谷歌后来转向了其他更先进的大数据技术,但 MapReduce 仍然是我们其他人进入处理庞大数据集世界的一个很好的起点。一个流行的用于实现 MapReduce 和类似算法的开源框架叫做 Hadoop,幸运的是,它的语言无关性意味着你可以使用 PHP 和 Hadoop 来加入大数据分析师的世界。
Hadoop 简介
Hadoop 是一个 Apache 项目,它为大型数据集的大规模分布式并行处理提供了一个与语言无关的框架。它可以从单台机器扩展到包含数千台服务器的集群,并负责管理所有机器上的处理(包括处理可靠性问题,如进程或机器中途失败)。Hadoop 旨在让您轻松创建处理任务的组成部分,将它们指向一些数据,然后在它运行您的任务并交付您的结果时坐下来。最初创建它是为了提供 MapReduce 算法的实现,但它也可以用于实现许多其他分布式任务。
关于 MapReduce
MapReduce 获取一个数据集,将它分解成小块,针对这些小块运行某种算法(通常是并行的),然后将这些单个任务的结果简化为最终结果。在谷歌的 MapReduce 版本中,应用于数据的算法将数据映射成一组键-值对,然后通过按键对数据进行分组并针对这些组映射进一步的归约函数来进行归约。在 Google 关于这个主题的开创性论文“MapReduce:大型集群上的简化数据处理”中描述的 MapReduce 比这个更加细致和结构化,并提供了确保一致性、允许失败和重试等功能。MapReduce 本质上不是一个函数式编程概念,它可以在所有编程范例中实现。
您应该还记得前几章中的术语 map 和 reduce,在这些章节中,您看到了array_map
和array_reduce
,由此,一个更非正式的 MapReduce 版本变得很常见,尤其是在函数式编程的实践者中。将数据分割成块,将一些函数映射到并行的函数,然后提供一个归约函数来生成最终输出的应用被认为是 MapReduce 的广义类。您将使用这种更一般的理解来构建一个简单的功能性应用,而不使用键值对等等;然而,如果你想更全面地探索 Hadoop 中“适当的”MapReduce,那么你可以在本章末尾的“进一步阅读”部分找到更多的细节。
如果这个任务听起来有点像你在第五章中写的并行编程工作,那是因为它就是!如果您还没有阅读这一章,我建议您现在就去阅读,然后再继续阅读。在这个平行的例子中,你将莎士比亚的全部作品分成几个块,“映射”一组分析函数来得到每个块的一些结果,然后将这些单独的结果集“简化”成一组代表整个文本的结果。这就是这种 MapReduce 任务的本质,您将使用完全相同的示例分析来学习如何使用 Hadoop 在 PHP 中实现 MapReduce。
如前所述,Hadoop 是语言无关的。作为开发人员,您可以编写和实现您的地图,并以任何方式减少工作。Hadoop 只是提供了一个框架来启动进程,在它们之间传递数据,从它们那里收集输出,以及管理和监控进程。您将把您的流程编写为 PHP 脚本,这些脚本通过STDIN
流从 Hadoop 接收数据,对其进行处理,然后将结果发送回STDOUT
。
安装 Hadoop
Hadoop 只在 Linux 和 Windows 上得到官方支持,尽管有些已经成功地在 MAC 上运行。虽然官方对 Windows 的支持很强,但实际应用中的最佳性能通常是在流线型的 Linux 集群上找到的。Hadoop 旨在跨服务器集群运行作业,这就是它用于大数据的原因。然而,幸运的是,它也可以在一台台式机上运行,并对“小数据”进行操作,这对学习和开发很有用。
根据您的系统和您想用 Hadoop 做什么,安装 Hadoop 可能会非常复杂。以下是一些基本安装指南。不过,出于本书的目的,您将简化整个安装过程,并使用一个已经安装了 Ubuntu 的虚拟机,该虚拟机已经预装了 Hadoop,并且主要配置为用作单机集群。虚拟机由 Bitnami 提供,可以免费下载。你需要一个合适的管理程序;Bitnami 虚拟机将在 VirtualBox、VMware 和 KVM 虚拟机管理程序中运行。如果你不熟悉虚拟化软件,我建议使用 VirtualBox,这是一个简单的免费桌面应用,很容易上手。
工具
- 主 Hadoop 网站
- 在 Windows 上安装 Hadoop
- 在 Linux 上安装 Hadoop
- 主要 Hadoop 文档
- Bitnami Hadoop 虚拟机下载页面
- Bitnami Hadoop 虚拟机文档
- Bitnami VM 常见问题解答
- 下载 VirtualBox
- VirtualBox 主要文档
在撰写本文时,Bitnami 提供了两个版本的 VM,一个基于 Debian,另一个基于 Ubuntu。这里您将使用 Ubuntu 版本,尽管 Debian 版本以类似的方式运行。从前面给出的链接下载 Bitnami Ubuntu 虚拟机,将其导入 VirtualBox 或您首选的虚拟机管理程序,并启动新的虚拟机。一旦启动,您应该会看到如图 9-1 所示的屏幕。
图 9-1。
vm_console.png
记下它显示的 IP 地址(在我的例子中是 192.168.0.22 ),因为您很快就会用到它。如您所见,默认用户名是 bitnami,密码也是 bitnami,所以使用这些详细信息登录。您将配置 VM 使您能够ssh
进入其中,这将允许您更容易地复制和粘贴命令和数据。运行清单 9-1 中的命令。
# Log in at the console, the default credentials are:
# bitnami:bitnami
# you will be prompted to change your password the first
# time you log in. You should change it, as everyone on your
# local network will be able to access your VM and hence
# able to SSH into it once SSH is enabled.
# We need to move the SSH config into place
sudo mv /etc/init/ssh.conf.back /etc/init/ssh.conf
# and then start SSH.
sudo start ssh
# SSH will now be started automatically each time the VM is
# started.
Listing 9-1.
enable_ssh.txt
如果您之前没有记下 IP 地址,您可以在命令行中键入ifconfig
来查看它。如果您愿意,现在可以从 VirtualBox 控制台注销,但不要关闭虚拟机。Bitnami VM 基于 Ubuntu 14.04 LTS(长期支持),因此只附带 PHP5 作为标准。您想要 7.1 版本,那么让我们ssh
进入 VM 并安装它,如清单 9-2 所示。
# SSH into the VM. Change the xxx's for your VM's IP address
ssh xxx.xxx.xxx.xxx -l bitnami
# Install the commands that will let us add a software repository
sudo apt-get install software-properties-common
# Now add the repository containing PHP and install PHP 7.1
sudo add-apt-repository ppa:ondrej/php
sudo apt-get update
sudo apt-get install php7.1
# Check that we have the right version now
php -v
Listing 9-2.
install_php.txt
Hadoop 对驻留在自己的文件系统 HDFS 上的数据进行操作。HDFS 被设计成一个可扩展的文件系统,可以跨越机器集群。您将编写一个 Hadoop MapReduce 作业来处理莎士比亚全集,但首先您需要将它放入虚拟机上的 HDFS 实例,在此之前,您需要将它放入虚拟机上的普通文件系统。可以使用 VirtualBox 设置共享文件夹,以便在主机操作系统和 Bitnami 虚拟机之间移动数据,也可以在虚拟机上安装和配置 Samba 或其他网络文件存储软件,以便能够访问其文件系统。然而,对于这个简单的例子,您将采用一种简单的方法,即打开一个文本编辑器(nano ),将您想要的文本/数据直接复制并粘贴到您的 SSH 会话中。参见清单 9-3 。
# By default the "namenode" in Hadoop is in safemode so
# we can't work with the file system. Let's turn that off.
hadoop dfsadmin -safemode leave
# We'll make a directory to put our data in
hadoop fs -mkdir shakespeare
# We can list the files to see where we're at
hadoop fs -ls /user/hadoop/
# The quickest way to get data onto the server is to copy and
# paste it into a text file, so open one, copy the contents
# from your local machine and paste it in.
nano all_shakespeare.txt
# The file we just created exists only on the VM's ordinary
# file system.
# Let's copy it into the HDFS directory we created above.
hadoop fs -copyFromLocal all_shakespeare.txt shakespeare/
# If we want to check the contents, we can "cat" the file
hadoop fs -cat shakespeare/all_shakespeare.txt
Listing 9-3.
hdfs_commands.txt
在 PHP 中创建 Hadoop 作业
Hadoop 是用 Java 编写的,用这种语言有很多方法可以创造就业机会。对于 PHP 用户,Hadoop 提供了一个“流”服务,允许你的 PHP 脚本与之交互。您将地图和 reduce 作业编写为 PHP 脚本,这些脚本通过STDIN
将输入作为文本,并将输出写入STDOUT
。Hadoop 负责分割数据,启动必要的并发地图作业,为它们提供少量数据,接收它们的部分输出,然后将其提供给 reduce 作业,并将其输出写入磁盘。所以,首先你会看到你将要使用的 PHP 脚本,然后如何用 Hadoop 运行它们。您将对 Shakespeare 进行与第五章中的并行编程示例相同的分析,所以如果您跳过了这一部分,请快速阅读,这样我们就在同一页上了。
与并行编程示例一样,您将任务分成两个脚本。worker 脚本,即 MapReduce 中的“map ”,类似于您之前编写的client.php
脚本。因为 Hadoop 承担了启动 worker (map)进程、控制数据流等繁重的工作,所以您的另一个脚本只需实现 reduce 函数,因此比前一个版本简单得多。同样,您将把函数捆绑到一个助手脚本中。现在就来看看那些(见清单 9-4 ,清单 9-5 ,清单 9-6 )。
#!/usr/bin/env php
<?php
require('job_functions.php');
# Compose our analysis function
$hadoop_analyze = compose(
$get_stream,
$only_letters_and_spaces,
'strtolower',
$analyze_words,
'json_encode'
);
# Run the analysis function on the input from Hadoop on STDIN
# and write the results to STDOUT
fwrite( STDOUT, $hadoop_analyze(STDIN) );
Listing 9-4.
map_job.php
#!/usr/bin/env php
<?php
require('job_functions.php');
# Compose our reduce function
$reduce = compose (
$get_stream_results,
$combine_results,
$sort_results,
'array_reverse',
$top_ten
);
# Call our reduce function on the results of the map jobs which Hadoop
# provides on STDIN, and print out the final result which Hadoop will
# save to disk
print_r( $reduce(STDIN) );
Listing 9-5.
reduce_job.php
<?php
# All functions in this file are the same as in the Parallel example in
# Chapter 5, except for $get_stream and $get_stream_results
# Hadoop splits our input and provides it to each instance of the map job as
# a stream of text. Let's grab that text.
$get_stream = function ($stream) {
return stream_get_contents( $stream );
}
# Hadoop sends the results from our map jobs (in no particular order) to
# the reduce job as a stream of lines of text. This function reads the
# stream and formats it into an array suitable for our combine_results
# function
$get_stream_results = function ($stream) {
# Map...
return array_map(
# ...json_decode...
function ($string) { return json_decode($string, true); },
# ... onto the contents of the stream which have been
# exploded into an array to make it easier to parse by
# the following functions
explode( PHP_EOL , stream_get_contents( $stream ) )
);
};
# All of the following functions are unchanged from the parallel script
$analyze_words = function ($string) {
$array = preg_split('/ /i', $string, -1, PREG_SPLIT_NO_EMPTY);
$filtered = array_filter($array, function ($word) {
return (
preg_match('/[shakespeare]/', $word) != false)
&& (similar_text($word, 'William is the best bard bar none') > 1)
&& (metaphone($word) == metaphone('bard'))
&& ( (strlen($word) > 3 )
);
});
return array_count_values($filtered);
};
$only_letters_and_spaces = function($string) {
return preg_replace('/[^A-Za-z]+/', ' ', $string);
};
$sort_results = function($array) {
asort($array, SORT_NUMERIC);
return $array;
};
$top_ten = function ($array) {
return array_slice($array, 0 ,10);
};
$combine_results = function ($results) {
return array_reduce($results, function($output, $array) {
foreach ($array as $word => $count) {
isset($output[$word]) ?
$output[$word] += $count :
$output[$word] = $count ;
}
return $output;
}, []);
};
function identity ($value) { return $value; };
function compose(...$functions)
{
return array_reduce(
$functions,
function ($chain, $function) {
return function ($input) use ($chain, $function) {
return $function( $chain($input) );
};
},
'identity'
);
}
Listing 9-6.
job_functions.php
注意map_job.php
和reduce_job.php
的#!
“哈什邦”第一行。如果您不熟悉命令行脚本,这将告诉 shell 使用什么程序来运行脚本。但是,您没有提供 PHP 可执行文件的直接路径,而是提供了一个环境变量,该变量(如果 PHP 已经正确安装)将指向 PHP 可执行文件。这意味着,如果您在另一台安装了 PHP 的机器上运行您的脚本,它应该仍然可以正常运行,无需任何更改。
总的来说,你已经把你的脚本组织得有点像一个大的伪函数,用STDIN
作为参数,用STDOUT
作为返回值。如果您假设这些标准流是“纯”的,并保证每次都能正确工作,那么以这种方式保持您的脚本结构化意味着 Hadoop 可以处理所有不纯的 I/O,并且您可以用函数的方式推理您的脚本。事实上,Hadoop 处理了许多可能导致脚本挂起、失败或行为不可预测的副作用,并且它可以处理在必要时重新运行它们以获得您期望的输出(或者至少在不可能时警告您)。
Hadoop 可以很好地运行你的脚本,而不用把它们转移到 HDFS,所以你需要做的就是让它们可执行。参见清单 9-7 。
# Create a directory for our scripts
mkdir scripts
# Edit the files and copy/paste the scripts into them
nano scripts/map_job.php
nano scripts/reduce_job.php
# Make sure both we and Hadoop can run them by adding
# execute permissions for everyone. If you install Hadoop on
# another system, depending on the sensitivity of your system
# and scripts, and which user you run Hadoop as, you may want
# to change who has execute permission here.
chmod a+x scripts/*.php
# Add the functions file. This is "require"'d by the scripts
# and not exectued directly, so does not need execute permission
nano scripts/job_functions.php
# We can test that it all works by using the Standard Streams
# that are available to all shell scripts, using redirection
# and piping.
php scripts/map_job.php < all_shakespeare.txt | php scripts/reduce_job.php
Listing 9-7.
setup_scripts.txt
如果您运行最后一个命令,您应该会发现您的脚本运行了,并且您得到了您期望的输出(参见第五章确认这一点)。但是你可以看到,这里没有涉及 Hadoop。这是可行的,因为 Hadoop 使用的流是任何 shell 脚本都可用的标准流。在前面的命令中,<字符将all_shakespeare.txt
的内容发送给map_job.php
的STDIN
,其结果被打印到STDOUT
,并且|符号通过管道将它们发送到reduce_job.php
的STDIN
。那么,为什么要为 Hadoop 费心呢?
在这个命令中,您只运行了map_job.php
的一个副本,而这个副本已经一次性处理了整个莎士比亚。虽然它适用于这么小的数据集,但它不适用于太大而不适合内存的数据集(更不用说太大而不适合单台机器的磁盘了!).Hadoop 处理将数据分割成可管理的块,旋转多个并发地图作业,并在它们工作时管理/监控它们。让我们看看如何用 Hadoop 运行脚本(参见清单 9-8 )。
# We need to know the location of the Hadoop streaming service.jar
# file. The path I've used below works at the time of writing,
# but if the version or path have changed when you try you'll need
# to locate it yourself. To do that, install and run the locate command.
sudo apt-get install locate
sudo updatedb
locate hadoop-streaming
# Once you've got the location, we're ready to run hadoop. The following
# command is split over several lines using the \ character, make sure
# you include all parts when you run it.
#The first line tells Hadoop what type of application we want to run.
# The -input line specifies the HDFS directory where our data resides
# (it will assume all files in that directory contain input data).
# The -output line specifies an HDFS output directory for the results, it
# must not already exist.
# The -mapper and -reduce lines specify
# our map and reduce scripts. Use the full directory path/filename.
hadoop jar /opt/bitnami/hadoop/share/hadoop/tools/lib/hadoop-streaming-2.8.0.jar \
-input shakespeare \
-output job_output \
-mapper /home/bitnami/scripts/map_job.php \
-reducer /home/bitnami/scripts/reduce_job.php
# Once Hadoop has run, you can examine the output directory
hadoop fs -ls /user/hadoop/job_output
# The main output is stored in part files, for us there is only one.
hadoop fs -cat /user/hadoop/job_output/part-00000
Listing 9-8.
hadoop_commands.txt
当您的 Hadoop 作业正在运行时,它会将各种细节记录到 shell 中,以便您可以尝试跟踪它在做什么。如果你想要一种更理智的方式来观察事物,你很幸运。Hadoop 在http://xxx.xxx.xxx.xxx/cluster/apps
(其中xxx
是您的 Bitnami 虚拟机的 IP 地址)有一个 web 界面,它允许您查看正在运行的任何作业的状态。默认用户名是 user,密码是 bitnami。图 9-2 显示了工作运行时的 web 界面。Hadoop 开发人员仍在学习响应式设计,因此宽屏显示器是一个优势!
图 9-2。
hadoop_web_interface.png
如果 Hadoop 在尝试运行您的作业时遇到任何问题,它会通过向 shell 发送错误消息、堆栈信息和其他文本来提供帮助。这使得找到实际的错误变得有点棘手。关键是向上滚动,直到遇到常规输出,然后查看错误的第一行;这通常是信息最丰富的一行。
如果一切顺利,您应该会看到类似清单 9-9 的输出,最后是一组关于您的工作的统计数据。
packageJobJar: [/tmp/hadoop-unjar3885899711350629262/] [] /tmp/streamjob5472160291450525372.jar tmpDir=null
17/06/14 19:45:56 INFO client.RMProxy: Connecting to ResourceManager at /0.0.0.0:8032
17/06/14 19:45:57 INFO client.RMProxy: Connecting to ResourceManager at /0.0.0.0:8032
17/06/14 19:45:57 INFO mapred.FileInputFormat: Total input files to process : 1
17/06/14 19:45:57 INFO mapreduce.JobSubmitter: number of splits:2
17/06/14 19:45:58 INFO mapreduce.JobSubmitter: Submitting tokens for job: job_1497468403783_0001
17/06/14 19:45:58 INFO impl.YarnClientImpl: Submitted application application_1497468403783_0001
17/06/14 19:45:58 INFO mapreduce.Job: The url to track the job: http://localhost:8088/proxy/application_1497468403783_0001/
17/06/14 19:45:58 INFO mapreduce.Job: Running job: job_1497468403783_0001
17/06/14 19:46:08 INFO mapreduce.Job: Job job_1497468403783_0001 running in uber mode : false
17/06/14 19:46:08 INFO mapreduce.Job: map 0% reduce 0%
17/06/14 19:46:19 INFO mapreduce.Job: map 100% reduce 0%
17/06/14 19:46:25 INFO mapreduce.Job: map 100% reduce 100%
17/06/14 19:46:26 INFO mapreduce.Job: Job job_1497468403783_0001 completed successfully
17/06/14 19:46:26 INFO mapreduce.Job: Counters: 49
File System Counters
FILE: Number of bytes read=353
FILE: Number of bytes written=417495
FILE: Number of read operations=0
FILE: Number of large read operations=0
FILE: Number of write operations=0
HDFS: Number of bytes read=3767109
HDFS: Number of bytes written=203
HDFS: Number of read operations=9
HDFS: Number of large read operations=0
HDFS: Number of write operations=2
Job Counters
Launched map tasks=2
Launched reduce tasks=1
Data-local map tasks=2
Total time spent by all maps in occupied slots (ms)=16341
Total time spent by all reduces in occupied slots (ms)=3773
Total time spent by all map tasks (ms)=16341
Total time spent by all reduce tasks (ms)=3773
Total vcore-milliseconds taken by all map tasks=16341
Total vcore-milliseconds taken by all reduce tasks=3773
Total megabyte-milliseconds taken by all map tasks=16733184
Total megabyte-milliseconds taken by all reduce tasks=3863552
Map-Reduce Framework
Map input records=157244
Map output records=2
Map output bytes=341
Map output materialized bytes=359
Input split bytes=234
Combine input records=0
Combine output records=0
Reduce input groups=2
Reduce shuffle bytes=359
Reduce input records=2
Reduce output records=13
Spilled Records=4
Shuffled Maps =2
Failed Shuffles=0
Merged Map outputs=2
GC time elapsed (ms)=261
CPU time spent (ms)=1610
Physical memory (bytes) snapshot=599236608
Virtual memory (bytes) snapshot=5641613312
Total committed heap usage (bytes)=460849152
Shuffle Errors
BAD_ID=0
CONNECTION=0
IO_ERROR=0
WRONG_LENGTH=0
WRONG_MAP=0
WRONG_REDUCE=0
File Input Format Counters
Bytes Read=3766875
File Output Format Counters
Bytes Written=203
17/06/14 19:46:26 INFO streaming.StreamJob: Output directory: job_output
Listing 9-9.hadoop_output.txt
如果您cat
了part-00000
文件,您应该得到如清单 9-10 所示的输出。
Array
(
[beard] => 76
[buried] => 43
[bright] => 43
[bred] => 36
[breed] => 35
[bird] => 34
[bride] => 30
[board] => 15
[broad] => 15
[bread] => 15
)
Listing 9-10.
part-00000.txt
正如你所看到的,它与第五章中的本地 PHP 并行版本是相同的,尽管注意到相同分数的单词的排序是不同的(例如,buried 和 bright,或者 board,broad 和 bread)。这是因为当运行并行任务时,您不能总是确定哪个进程将首先返回,因此也不能确定以什么顺序将结果传递给 reduce 作业。如果在运行之间精确复制输出结果的格式/顺序很重要,那么使用这样的任务,您可以在减少结果之前或之后(根据需要)对结果进行规范化(例如通过按字母顺序对键进行排序)。
从 Hadoop 输出中的“分割数量”和“启动的地图任务”行,您可以看到它决定(根据默认设置)将您的数据分成两个块,并并行运行两个作业。当然,您可以修改 Hadoop 的配置,使其创建更小或更大的块以及不同数量的地图工作者。
你还会注意到 Hadoop 比你的 PHP 并行版本慢很多。事实上,这比您之前通过直接执行来测试脚本要慢得多(那是通过一个单独的map_job.php
实例来运行整个文本)。为什么呢?因为 Hadoop 带来了很大的开销。对于这样的玩具问题来说绝对是矫枉过正。无论莎士比亚在他的时代是一个多么多产的作家,他一生的产出(好吧,无论如何是最好的部分)可以连接成几兆字节的文本。Hadoop 旨在处理大得多的数据集,只有当您对数百千兆字节的数据进行复杂计算时,它的开销才开始得到回报。事实上,在 Hadoop 发挥作用之前,更简单的问题通常需要几 TB 的数据。但是如果你有那么多数据,而且你有 PHP,那么 Hadoop 绝对值得作为一个有用的工具来看待。如果您没有那么多数据,这仍然是了解基于集群的处理的一个好方法。
正如您可能已经猜到的那样,您仅仅触及了 Hadoop(或者实际上是 MapReduce)的皮毛。让我们回顾一下我在一本关于函数式编程的书中加入 Hadoop 和大数据章节的原因。当您处理大量数据时,这意味着您需要以非顺序的方式处理数据(通常跨不同的机器),您需要能够做到以下几点:
- 以抽象的方式思考你的代码
- 控制副作用并管理干净的代码流
- 将代码分成可广泛重用和高度优化的包
在本书讲到这里之后,这些是你现在应该与函数式编程联系在一起的特征。明确一点,你不需要使用函数式编程来与 Hadoop 或其他分布式数据处理系统协同工作;事实上,MapReduce 不是一个函数概念(尽管它的基本特征很好地映射到函数结构上)。但函数式编程正越来越多地被 Twitter 等公司用于处理大型数据处理和流程,在这些公司,保持基本的业务逻辑在规模上是很难的。他们可能不会使用 PHP,但是你会,同样的原则也适用。
如果这个简单的例子吸引了你,下面的参考资料将让你了解更多关于 MapReduce、Hadoop 以及在 HDFS 中使用 PHP 的知识。
进一步阅读
- 一篇阐述我的观点的文章,解释了为什么 MapReduce 本身不是函数式编程
- 概述如何在 Hadoop 中实现完整 MapReduce 算法的教程
- PHDFS,一个帮助你直接从 PHP 访问 HDFS 的库
- 你用来处理 PHP 脚本的 Hadoop 流应用的更多细节
十、后记
现在去哪里?
如果你已经读到这里,谢谢你。我真诚地希望这本书能引起你的兴趣,至少对你未来的 PHP 编程有所启发。如果是的话,我鼓励你现在就根据本书中的一个或多个主题开始编码,趁它们还在你的脑海中。比我聪明得多的人已经证明,学习后相关活动发生得越早,就越容易长期记住信息和技术。
如果您还没有使用键盘,请不要忘记浏览下面的附录。这里面可能有你感兴趣或有用的东西(要么现在就用,要么将来就知道)。
如果你感觉有点“卡住了”,考虑做一个或多个下面的练习怎么样:
- 浏览一下附录 c 中的资源,给自己列一个感兴趣的“要学习”的主题列表,扩展你的函数式编程知识。您还可以查看非 PHP 主题,并尝试用 PHP 实现一些概念。
- 你是否有一些错误或有问题的代码你一直在推迟修复?或者可能有一个陈旧的应用在等待重写?为什么不尝试用函数式 PHP 重新实现代码(或者只是关键/有问题的部分)?
- 尝试参加在线代码竞赛,解决在线谜题,或者使用你从本书中学到的功能技巧练习“面试任务”。
- 有一段时间脑子里有一个“副业”,但是没有动力去做它?为什么不现在就开始使用函数式编程呢?在完成一项明确的、有价值的任务时学习一些东西,既能帮助你学得更快,又能帮助你增加完成任务的动力。
- 如果你只使用一种范式(例如,过程式编程)在 PHP 中编程,不要假设函数式编程是最好的,会非常适合你的用例。拿起一本书或阅读一个关于 PHP 中使用的其他范例(如面向对象编程)或 PHP 之外的范例(如面向方面或基于约束的编程)的 web 教程。你将增加你的技能工具箱,它将帮助你看到函数式编程的缺点和优点,你甚至可以想出更好地混合不同范例的方法来适应手边的任务。
如果你已经尝试过一些编程,你不确定你是否在正确的轨道上,并且你没有人可以直接询问,获得反馈的最好方法之一是将你的代码发布到互联网上。如果你没有得到回应,你可能是在正确的轨道上。但是把一个分号放到网上的某个地方,就会有人等着纠正你!或者,给同事或朋友买一本这本书,让他们和你一起结对编程。
提供反馈并获得帮助和支持
电子邮件:author@active-net.co.uk
你对这本书的反馈——好的或坏的,基本的或琐碎的——是被请求的,也是非常受欢迎的。不要仅仅局限于第六章样本代码捆绑中的照片里我的猫有多可爱(好像你还没有把它设置为你的桌面壁纸!).我想知道以下内容:
- 你对这本书有什么看法,总体上还是就特定部分而言
- 如果有任何领域没有足够的深度(或太多的细节)
- 如果你期待的任何话题没有出现或者任何信息不清楚
- 如果你在《现实生活》中使用了书中介绍的任何技巧(我会很有兴趣知道!)
同样,如果您在运行示例代码时有任何问题,或者在实现所讨论的技术时有任何问题,请给我写信,我会看看是否有任何我可以帮忙的地方。
如果你觉得这本书有用,我希望你有,你可以通过在推特上发布你对这本书的想法,在 PHP 小组和 meetups 上提到它,或者在博客上讲述你的新功能技能(厚颜无耻地提到你在哪里找到它们),来帮助其他人获得同样令人满意的功能性益处。