十三、用 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函数来执行排队,并分别使用headtail函数来执行队列中的第一个项目和出列器。以下是前面代码的输出:

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 对编写函数式编程代码有很好的支持,有了它,我们还可以编写函数式数据结构和算法实现,正如我们在本书中尝试展示的那样。在不久的将来,它可能会在优化和提高应用程序效率方面派上用场。