一、介绍
函数式编程通常不会与 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)
函数),但是在很多情况下,这是一个更实用的组合多元函数的方法。
结论
你现在开始写功能代码了。在这一章中,您了解了以“函数式”方式构造函数的各种方法,并了解了诸如递归和部分函数等技术如何让您编写更灵活的函数。您可以使用到目前为止已经看到的技术来创建其他常见的程序控制结构,您将在阅读本书的其余部分时看到这些内容。在下一章,你将开始学习一些更高级的函数式编程主题。