思考并回答以下问题:
- 为什么要使用诸如map,filter和reduce之类的函数代替foreach循环?
- 一个函数调用多次,函数状态会在内存中新生成一个“副本”吗?
Map,Filter,Reduce
Listing 3-1 map_filter_reduce.php
1 |
|
Listing 3-2 map_filter_reduce-output.txt
1 | Array |
递归函数
Listing 3-3 shopping_list1.php
1 |
|
Listing 3-4 foreach.php
1 |
|
Listing 3-5 foreach-output.txt
1 | Total items to purchase : 34 |
Listing 3-6 shopping_list2.php
1 |
|
Listing 3-7 foreach2-output.txt
1 | PHP Fatal error: Uncaught Error: Unsupported operand types in foreach.php:11 |
Listing 3-8 recursive.php
1 |
|
Listing 3-9 recursive-output.txt
1 | List 1 : 34 |
每次您调用一个函数(无论是从内部调用还是从外部调用),则在内存中创建函数状态的新“副本”。这意味着该函数的每次调用与其他相同函数的调用实际上是分开的。调用它们时,每个函数调用的状态被放置到内存中的调用堆栈中,查看调用堆栈可以帮助您可视化递归。
Listing 3-10 recursive_stack.php
1 |
|
Listing 3-11 recursive_stack-output.txt
1 | --- |
递归是好的,但是您必须小心以确保递归循环一定会终止于一点。
Listing 3-12 forever.php
1 |
|
Listing 3-13 forever-output.txt
1 | PHP Fatal error: Allowed memory size of 1073741824 bytes exhausted (tried to allocate 262144 bytes) in forever.php on line 6 |
每次调用函数时,都会在其中创建一个副本内存,在这种情况下,由于无法退出函数,每个内存消耗越来越多的内存调用,并且永远不会通过退出函数来释放。请注意,我为脚本显式设置了内存限制ini_set。与Web脚本不同,PHP CLI脚本默认情况下没有内存限制。没有限制,这脚本可能会消耗所有可用内存,从而使您的机器瘫痪。
调用函数时,在函数中使用的每个变量,每个调试语句以及分配的每个其他资源都占用宝贵的内存。调用一次,它的总和可能不多,但调用递归进行数百或数千次,可能很快成为一个问题。因此,您应始终尝试将递归函数中的每种状态形式保持在绝对最小值。
尾递归是递归的一种形式,其中递归调用是函数的最后一部分。在许多语言中,编译器可以优化尾部递归,因为它不需要“堆栈框架”即可让编译器将状态存储回去。不幸的是,PHP虚拟机没有提供这样的优化,因此,我不会详细介绍尾递归。 在下一章中,您将转而关注“蹦床”,通过自动将递归函数展平到循环中,您可以得到类似的优化。
实现递归函数
Listing 3-14 all_recipes.php
1 |
|
Listing 3-15 all_recipes-output.txt
1 | Array |
Listing 3-16 recipe_functions.php
1 |
|
Listing 3-17 all_recipes_recursive.php
1 |
|
Listing 3-18 all_recipes_recursive_output.txt
1 | Showing 5 of 25 items: |
Listing 3-19 new_ingredients.php
1 |
|
Listing 3-20 new_ingredients-output.txt
1 | Showing 11 of 25 items: |
偏函数
在第1章中,您研究了函数式编程如何体现OOP的SOLID原理。一
在这些原则中,接口隔离原则(ISP)意味着仅对于
完成当前任务应该是您需要传递给函数的任务。
在上一节中,考虑recipe_functions.php中的print_first函数。需要两个
参数,要打印的项目数组和要打印的项目数。通常这是合理的
需要两个参数,因为通常它们对于给定任务都会有所不同。但是,如果您正在编写新的
网站theTopFiveBestEverListsOfStuff.com,您将永远只想打印第一个
列出的任何五个项目。当然,您可以在整个过程中重复输入print_first($ list,5)
脚本。但是,当排在前五名的名单的市场饱和时,您需要继续前进,
有史以来十大最佳市场,您需要找到所有这些5并将其替换为10。如果您不小心输入了4
而不是5或1而不是10,您在一下午的市场份额将损失一半。
您当然可以用变量$ count替换5,然后在需要时设置$ count = 10。但
在全局范围内执行此操作意味着要进行额外的工作,以确保其他功能范围内的调用都可以使用它,并且
当另一个程序员在某处意外地将$ count用作循环计数器时,奇怪的错误将比比皆是。
偏函数可为您解决这些问题。偏函数是采用现有功能并通过将值绑定到一个(或多个)参数来减少其复杂性。 放另一个方式,部分功能通过修复一个或多个现有功能的一个或多个功能,可提供现有功能的更具体版本参数设置为特定值,从而减少了调用它所需的参数数量。让我们创建一个在前五名站点上打印列表的部分功能。 请参见清单3-21和清单3-22。
Listing 3-21 top_five.php
1 |
|
Listing 3-22 top_five-output.txt
1 | Showing 5 of 11 items: |
您现在可以在整个网站上愉快地使用print_top_list部分功能,在
知道a)您可以随时在一个中央位置将数字5更改为10,b)您仍然可以
受益于对基础print_first函数的任何更新或更改,并且c)您仍然可以调用
在任何其他脚本中,print_first函数直接使用任何数字表示第二个参数中的第二个参数
使用相同的功能,但需要不同的编号。
尽管这展示了部分函数的好处,但是您手动创建它的过程有点笨拙
并且不可重用。因此,让我们成为真正的函数式程序员,并创建一个函数来创建您的部分函数
职能!我在第二章中谈到了高阶函数。提醒一下,这些功能可以
其他功能作为输入和/或作为输出返回。您将定义一个名为partial的函数,该函数需要一个
函数和一个或多个绑定到它的参数,并吐出一个现成的部分函数供您使用。
参见清单3-23,清单3-24和清单3-25。
Listing 3-23 partial_generator.php
1 |
|
Listing 3-24 partial.php
1 |
|
Listing 3-25 partial-output.txt
1 | Showing 5 of 6 items: |
尽管我说过有关named的内容,但本示例使用了一个命名函数而不是闭包
功能更早。 这是为本书的范围而故意设计的; 您稍后将再次使用它,并且在
您正在编写的简单程序,将其用作命名函数意味着您不需要在每个程序中
您要从中调用的每个函数。 在程序中,您可能希望将其更改为闭包
给您带来好处。
如您所见,局部函数生成器允许您使用可重用的方式来创建多个局部函数
您可以根据需要使用它们,并制作了两个不同的部分($ print_top_five和$ print_best)。 您
可以使用此功能来减少任何功能的数量。 考虑清单3-26中的函数,
的Arity为4,您将减少2。 清单3-27显示了输出。
Listing 3-26 concatenate.php
1 |
|
Listing 3-27 concatenate-output.txt
1 | what is your name |
偏函数可帮助您将函数分解为单一用途,可重用和
可维护的功能。 它们使您可以共享更广泛的“单片式”功能的核心功能
几个不同的任务,同时仍然受益于功能的集中化。 他们
还允许您(如果需要)使用纯数学函数来实现奇偶校验,该函数仅接受一个
单论点。 在下一章中,您将研究curring,尽管我专注于食物,但它并不是
功能化印度菜,而是一种将多种功能自动分解为
一连串的单参数函数。
函数式表达式
函数式编程倾向于使用“函数式表达式”进行程序控制,而不是使用
传统的命令式控制结构,您已经间接查看了其中的一些示例。
您可以使用已经探索的技术来组合一些更有用的表达方式。
某些易于转换和理解的示例往往是数字函数。 毕竟,
函数式编程源于数学。 在许多语言中,函数inc和dec存在
用于递增和递减整数。 在PHP中,您习惯使用++和-运算符
而是,但是没有理由不能使用称为的函数编写自己的函数表达式
公司和十二月 您可能会很想创建清单3-28所示的这些函数来实现此目的(使用
输出如清单3-29所示)。
Listing 3-28 inc_dec.php
1 |
|
Listing 3-29 inc_dec-output.txt
1 | int(4) |
完全正确,但是让我们考虑使用部分函数技术的不同方法
您之前看过的。 参见清单3-30和清单3-31。
Listing 3-30 inc_dec_partial.php
1 |
|
Listing 3-31 inc_dec_partial-output.txt
1 | int(4) |
请注意,您可以根据需要混合和匹配命名函数和匿名函数
技术。 最初,您只需付出一点额外的努力,即可获得更大的灵活性,并轻松创建其他派生类
职能。 另一个示例可能是根据用例创建功能版本的能力。
例如,您和我可能会认为一打是12,但对面包师来说是13。请参见清单3-32和清单3-33。
Listing 3-32 dsl.php
1 |
|
Listing 3-33 dsl-output.txt
1 | int(24) |
这种创建功能的功能描述了他们将要做什么,而不是详细说明如何去做,
是使函数式编程非常适合创建领域特定语言的属性之一
(DSL)。 DSL是为特定应用量身定制的语言或现有语言的改编
“域”(例如,特定行业或软件类型)。
您已经讨论了一种通过减少现有功能的繁琐性来创建新功能的方法,但是如果您
想要通过合并多个现有功能来制作新功能?您可以在
其他带有中间变量的变量将输出从一个传递到下一个。或者,您可以连锁
通过直接使用一个函数作为下一个函数的参数将它们组合在一起。这是一种功能形式
组合,并且一如既往,有一种更好的“功能性”方法可以做到这一点。
假设您有一个秘密的公式可以计算出使世界上最好的温度的最佳温度
芒果雪糕。该公式计算您正在使用的芒果数量(例如6),将其加倍(12),取反
(-12),再加上2(-10°C)。您需要将此公式作为函数嵌入到运行的PHP软件中
您的冰淇淋机。但是,您会制作其他口味的冰淇淋,每种口味都有
自己独特的配方。因此,您需要从一组可重用的基本数学函数开始并进行组合
到专门用于芒果的配方中,同时仍留有空间轻松实现该配方
待会儿再来吃草莓冰淇淋。一种方法是将几个函数一起组成一个mango_temp
函数,如清单3-34和清单3-35所示。
Listing 3-34 sums1.php
1 |
|
Listing 3-35 sums1-output.txt
1 | -10°C |
可以,但是阅读起来不太直观。 由于每个函数都嵌套在前一个函数中,
您必须有效地从右向后阅读它,以了解执行顺序。
纯函数语言通常具有语法或函数,用于像这样将函数组合在一起,但是在
一种更易于阅读的方式。 PHP没有,但不要担心,因为创建自己的PHP很容易(请参见清单3-36)。
Listing 3-36 compose.php
1 |
|
Listing 3-37 sums2.php
1 |
|
Listing 3-38 sums2-output.txt
1 | -10°C |
您可以在链的开头(此输出的中间)看到身份函数,每个
后续功能依次作为每个“链”闭合的属性。
在第1章中,您查看了功能类型代码的样本。 我不想介绍构图
在那个阶段发挥作用,以免在早期使水变得浑浊。 但是,现在您知道了
组成,您可以重写该示例,如清单3-39所示。
Listing 3-39 example2.php
1 |
|
这些示例中要注意的一件事是,您的compose函数仅适用于
取一个参数。这是故意的,因为函数只能返回单个返回值。如果有功能
接受两个参数,compose函数将如何知道从何处使用单个返回值
上一个函数调用?
您可以使用我已经介绍过的部分函数之类的技术来创建单个函数
配合使用。当然,单个参数可以是数组或类似的数据结构,如果
您需要在函数之间移动数据集。强制执行单个参数还有助于确保您的
功能应保持简单并尽可能限制范围。但是,这通常很实用(有时
如果您正在使用其他人的功能或代码,则可以使用此功能)
具有多个参数的函数。函数式编程在这里也有介绍。你只是
将函数包装在另一个返回函数的函数中!清单3-40和清单3-41显示了使用PHP的
本机的str_repeat函数(带有两个参数:字符串和重复次数)
应该使这一点更加清楚。
Listing 3-40 strrepeat.php
1 |
|
Listing 3-41 strrepeat-output.txt
1 | The string is : ********** |
理解您在此脚本中所做的工作的关键是要认识到,当您在脚本中使用repeat_str(10)
compose语句,不是您要传入的函数。在函数名称后加上括号
在此处执行它,然后将其自身替换为返回值。 因此,您正在调用中的repeat_str(10)
编写语句定义,而repeat_str(10)返回的函数实际上是由
组成为参数。 repeat_str(10)返回一个接受一个参数的闭包(这是您所需要的)
(对于您的compose函数)为$ string,但通过使用($ count)绑定了第二个参数(10)。
当然,您不必这样做。 例如,您可以开始创建部分函数(例如
repeat_ten_times($ string)函数),但这是编写多arar函数的更实用的方法
在许多情况下。
总结
您现在就开始编写功能代码。 在本章中,您研究了各种结构化方式
以“函数式”方式运行函数,并研究了诸如递归和部分函数之类的技术如何使您
编写更灵活的功能。 您可以使用到目前为止所研究的技术来使其他通用
程序控制结构,当您阅读本书的其余部分时,将对其进行研究。 下一个
本章中,您将开始研究一些更高级的函数式编程主题。