十三、用 PHP 实现函数式数据结构
近年来,对函数式编程语言的需求超过了面向对象编程。其中一个核心原因是函数式编程(FP)具有内在的并行性。虽然 OOP 被广泛使用,但函数式编程在最近几年却取得了显著的成就。因此,Erlang、Elixir、Clojure、Scala 和 Haskell 等语言是程序员最流行的函数式编程语言。PHP 不在列表中,因为 PHP 被认为是一种命令式和面向对象的语言。尽管 PHP 对函数式编程有很多支持,但它主要用于 OOP 和命令式编程。FP 的核心本质是 lambda 演算,它表示数理逻辑和计算机科学中的一个形式系统,用于通过变量绑定和替换来表示计算。它不是一个框架或新概念。事实上,函数式编程早于所有其他编程范式。它已经存在了很长一段时间,将来也会存在,因为世界要求更多的并发计算和更快的处理语言。在本章中,您将学习使用 PHP 进行函数式编程,以及如何使用函数式编程实现数据结构。
用 PHP 理解函数式编程
与任何面向对象的编程语言不同,在面向对象的编程语言中,所有东西都是通过对象来表示的,函数式编程从函数的角度来考虑所有东西。OOP 和 FP 不是相互排斥的。面向对象编程通过封装和继承关注代码的可维护性和可重用性,而函数式编程不同于面向状态的命令式编程,它关注面向值的编程,它将计算视为纯粹的数学评估,并避免可变性和状态修改。在使用 OOP 时,挑战之一是我们创建的对象可以带来许多额外的属性或方法,无论我们是否在特定情况下使用它。以下是函数式编程的三个关键特征:
- 不变性
- 纯函数与引用透明
- 一等公民职能
- 高阶函数
- 功能组合(咖喱)
不变性的概念告诉我们,对象在创建后不会改变。在其整个生命周期内,它将保持不变。它有一个很大的优势,因为无论何时使用它,我们都不需要重新验证对象。此外,如果需要可变,我们可以创建对象的副本或创建具有新属性的新对象。
到目前为止,在这本书中,我们看到了许多使用代码块、循环和条件的数据结构和算法的示例。一般来说,这被称为命令式编程,其中需要定义执行的每个步骤。例如,考虑下面的代码块:
$languages = ["php", "python", "java", "c", "erlang"];
foreach ($languages as $ind => $language) {
$languages[$ind] = ucfirst($language);
}
前面的代码实际上将每个名称的第一个字符设置为大写。从逻辑上讲,代码是正确的,我们在这里一步一步地介绍了它,以便我们了解发生了什么。但是,可以使用函数式编程方法将其写成一行,如下所示:
$languages = array_map('ucfirst', $languages);
这两种方法的作用相同,但其中一种方法的代码块相对较小。后者称为声明式编程。命令式编程侧重于算法和步骤,而声明式编程侧重于函数的输入和输出以及递归(而不是迭代)。
函数式编程的另一个重要方面是它没有任何副作用。它是一个重要的特性,可以确保函数不会对输入的任何地方产生任何隐式影响。函数式编程的一个常见示例是在 PHP 中对数组进行排序。通常,参数是通过引用传递的,当我们得到排序数组时,它实际上会破坏初始数组。这是函数中的副作用示例。
在开始使用 PHP 进行函数式编程之前,让我们先了解一些函数式编程术语,我们将在下面几节中遇到这些术语。
一类函数
具有一流函数的语言允许以下行为:
- 将函数赋给变量
- 将它们作为参数传递给另一个函数
- 返回函数
PHP 支持所有这些行为,因此 PHP 函数是一流的函数。在我们前面的示例中,ucfirst
函数是一类函数的示例。
高阶函数
高阶函数可以将一个或多个函数作为参数,也可以作为结果返回函数。PHP 还支持高阶函数;我们上一个例子中的array_map
是一个高阶函数。
纯函数
纯函数是一个函数,其中对于输入 X,在任何情况下输出总是 Y。对于纯函数的相同输入,输出永远不会改变。因此,对于纯函数,运行时环境没有任何副作用或依赖性。
匿名函数
Lambda 函数或匿名函数是没有名称的函数。当用作一级函数(在变量中赋值)或回调函数时,它们非常方便,在回调函数中,我们可以在回调参数的位置定义函数。PHP 还支持匿名函数。
闭包
闭包与 lambda 函数非常相似,但基本区别在于闭包可以访问其外部范围变量。在 PHP 中,我们不能直接访问外部作用域变量。为了做到这一点,PHP 引入了关键字“use”,将任何外部作用域变量传递给内部函数。
咖喱
curry 是一种将接受多个参数的函数转换为一个函数链的技术,其中每个函数只接受一个参数。换句话说,如果一个函数可以写成f(x,y,z),那么这个函数的当前版本将是f(x)(y)(z)。让我们考虑下面的例子:
function sum($a, $b, $c) {
return $a + $b + $c;
}
在这里,我们编写了一个带有三个参数的简单函数,当使用数字调用时,它将返回数字的和。现在,如果我们将此函数写成 curry,它将如下所示:
function currySum($a) {
return function($b) use ($a) {
return function ($c) use ($a, $b) {
return $a + $b + $c;
};
};
}
$sum = currySum(10)(20)(30);
echo $sum;
现在,如果我们运行currySum
作为一个 currying 函数,我们将得到前面示例的结果 60。这是函数式编程非常有用的特性。
Earlier, it was not possible to call a function like f(a)(b)(c) in PHP. Since PHP 7.0, Uniform Variable Syntax allows immediate execution of a callable, as we saw in this example. However, to do this in PHP 5.4 and higher versions, we would have to create temporary variables in order to store the lambda functions.
部分应用
部分应用程序或部分函数应用程序是一种技术,用于减少函数的参数数量,或使用部分参数并创建另一个函数来处理其余参数,以便生成与同时使用所有参数调用时相同的输出。如果我们认为我们的 To0t0 函数是部分的,在这里它需要取三个参数,但是我们可以用两个参数调用它,稍后再添加剩下的参数。下面是代码示例。本例中使用的sum
函数来自上一节:
function partial($funcName, ...$args) {
return function(...$innerArgs) use ($funcName, $args) {
$allArgs = array_merge($args, $innerArgs);
return call_user_func_array($funcName, $allArgs);
};
}
$sum = partial("sum", 10, 20);
$sum = $sum(30);
echo $sum;
有时,我们会混淆 curry 和 partialapplication,尽管它们的方法和原则完全不同。
正如我们所看到的,在 PHP 中处理函数编程有很多事情要考虑。在 PHP 中从头开始使用函数式编程实现数据结构将是一个较长的过程。为了解决这个问题,我们将探索一个优秀的 PHP 函数编程库,名为Tarsana。它是开源的,附带 MIT 许可证。我们将探索这个库,并将其用作 PHP 中函数数据结构实现的基础。
Tarsana 入门
Tarsana 是由 Amine Ben Hammou 编写的开源库,可在 GitHub 上下载。它的灵感来自 RamdaJS,一个 JavaScript 函数编程库。它没有任何依赖项,并且有 100 多个预定义函数用于不同的目的。FP 中的函数分布在不同的模块上,有几个模块,如函数、列表、对象、字符串、数学、运算符和公共。Tarsana 可从 GitHub 下载(https://github.com/Tarsana/functional )或可通过 composer 安装。
composer require Tarsana/functional
一旦库被下载,我们必须通过导入Tarsana\Functional
名称空间来使用它,就像下面的代码一样:
use Tarsana\Functional as F;
Tarsana 的一个有趣特性是,我们可以将任何现有函数转换为 curried 函数。例如,如果我们想使用 Tarsana 使用我们的sum
函数,那么它将如下所示:
require __DIR__ . '/vendor/autoload.php';
use Tarsana\Functional as F;
$add = F\curry(function($x, $y, $z) {
return $x + $y + $z;
});
echo $add(1, 2, 4)."\n";
$addFive = $add(5);
$addSix = $addFive(6);
echo $addSix(2);
这将分别产生 7 和 13 的输出。Tarsana 还可以选择使用__()
功能保留占位符。以下示例显示占位符中提供的条目的数组 reduce 和数组总和:
$reduce = F\curry('array_reduce');
$sum = $reduce(F\__(), F\plus());
echo $sum([1, 2, 3, 4, 5], 0);
Tarsana 还提供了管道功能,我们可以从左到右应用一系列功能。最左边的函数可以有任何算术运算;其余函数必须是一元函数。配管的结果是不正确的。让我们考虑下面的例子:
$square = function($x) { return $x * $x; };
$addThenSquare = F\pipe(F\plus(), $square);
echo $addThenSquare(2, 3);
由于我们已经探索了 Tarsana 的一些特性,我们已经准备好使用 Tarsana 开始我们的功能数据结构。如果我们不想使用函数式编程,我们还将使用简单的 PHP 函数实现这些数据结构,这样我们就可以涵盖这两个部分。让我们从栈的实现开始。
实现栈
我们在第 4 章、构建栈和队列中看到了栈的实现。为简单起见,我们不再讨论整个栈操作。我们将直接使用函数编程实现 push、pop 和 top 操作。Tarsana 有许多用于列表操作的内置函数。我们将使用它们的内置函数来实现栈的功能操作。以下是实施方案:
require __DIR__ . '/vendor/autoload.php';
use Tarsana\Functional as F;
$stack = [];
$push = F\append(F\__(), F\__());
$top = F\last(F\__());
$pop = F\init(F\__());
$stack = $push(1, $stack);
$stack = $push(2, $stack);
$stack = $push(3, $stack);
echo "Stack is ".F\toString($stack)."\n";
$item = $top($stack);
$stack = $pop($stack);
echo "Pop-ed item: ".$item."\n";
echo "Stack is ".F\toString($stack)."\n";
$stack = $push(4, $stack);
echo "Stack is ".F\toString($stack)."\n";
这里,我们使用 Tarsana 的 append 函数进行 push 操作,最后一个函数用于 top 操作,而init
函数用于 pop 操作。以下代码的输出如下所示:
Stack is [1, 2, 3]
Pop-ed item: 3
Stack is [1, 2]
Stack is [1, 2, 4]
实现队列
我们可以使用 Tarsana 和用于列表操作的内置函数实现队列。我们还将使用数组表示队列,使用以下代码:
require __DIR__ . '/vendor/autoload.php';
use Tarsana\Functional as F;
$queue = [];
$enqueue = F\append(F\__(), F\__());
$head = F\head(F\__());
$dequeue = F\tail(F\__());
$queue = $enqueue(1, $queue);
$queue = $enqueue(2, $queue);
$queue = $enqueue(3, $queue);
echo "Queue is ".F\toString($queue)."\n";
$item = $head($queue);
$queue = $dequeue($queue);
echo "Dequeue-ed item: ".$item."\n";
echo "Queue is ".F\toString($queue)."\n";
$queue = $enqueue(4, $queue);
echo "Queue is ".F\toString($queue)."\n";
这里,我们使用append
函数来执行排队,并分别使用head
和tail
函数来执行队列中的第一个项目和出列器。以下是前面代码的输出:
Queue is [1, 2, 3]
Dequeue-ed item: 1
Queue is [2, 3]
Queue is [2, 3, 4]
现在,我们将把重点转移到使用简单的 PHP 函数而不是类和对象实现分层数据。由于函数式编程在 PHP 中仍然是一个新主题,分层数据的实现可能看起来很有挑战性,也很耗时。相反,我们将使用基本 PHP 函数以及一些基本函数编程概念(如一级函数和高阶函数)转换分层数据实现。那么,让我们实现一个二叉树。
实现树
我们将使用一个 PHP 数组和一个简单的基于递归函数的遍历来实现一个二叉树。我们只是用一个函数而不是一个类重写功能。以下是执行此操作的代码:
function treeTraverse(array &$tree, int $index = 0,
int $level = 0, &$outputStr = "") : ?bool {
if(isset($tree[$index])) {
$outputStr .= str_repeat("-", $level);
$outputStr .= $tree[$index] . "\n";
treeTraverse($tree, 2 * $index + 1, $level+1,$outputStr);
treeTraverse($tree, 2 * ($index + 1), $level+1,$outputStr);
} else {
return false;
}
return null;
}
$nodes = [];
$nodes[] = "Final";
$nodes[] = "Semi Final 1";
$nodes[] = "Semi Final 2";
$nodes[] = "Quarter Final 1";
$nodes[] = "Quarter Final 2";
$nodes[] = "Quarter Final 3";
$nodes[] = "Quarter Final 4";
$treeStr = "";
treeTraverse($nodes,0,0,$treeStr);
echo $treeStr;
如果我们查看前面的代码,我们只需修改遍历函数并将其转换为独立函数。这是一个纯函数,因为我们在这里不修改实际输入,即$nodes
变量。我们将在每个级别上构造一个字符串,并将其用于输出。我们现在可以将大多数基于类的结构转换为基于函数的结构。
总结
函数式编程对于 PHP 开发人员来说是相对较新的,因为从 5.4 版开始就添加了对其先决条件的支持。函数式编程的出现将要求我们理解范式,并在需要时编写没有任何副作用的纯函数。PHP 对编写函数式编程代码有很好的支持,有了它,我们还可以编写函数式数据结构和算法实现,正如我们在本书中尝试展示的那样。在不久的将来,它可能会在优化和提高应用程序效率方面派上用场。
版权属于:月萌API www.moonapi.com,转载请注明出处