PHP 8.5 刚刚发布,但推动 PHP 前进的工作仍在继续。PHP 8.6 的最新重大功能刚刚获得批准:部分函数应用 (PFA)。

核心要点

在 PHP 8.6 中,将能够创建一个闭包,该闭包仅委托给另一个函数,而无需写出整个闭包。

// 这
$underscore = str_replace(' ', '_', ?);

// 实际上等同于:
$underscore = fn(string $s): string => str_replace(' ' , '_', $s);

这两行代码将生成几乎相同的操作码,但前者更容易编写和阅读,因为它不需要重新声明所有类型和变量名。

任何函数(或方法)调用都可以使用两个占位符之一 ? 或 ...,以表明它只是“部分调用”该函数。或者说是“向其部分应用参数”。它也适用于位置参数和命名参数!

function complex(int $a, int $b, int $c, int $d): string { ... }

// 这会创建一个接受 2 个整数并返回字符串的闭包。
$f = complex(?, 2, ?, 4);

// 这会创建相同的闭包,但使用命名参数。
$f = complex(b: 2, d: 4, ...);

// 这会反转参数的顺序,$f 需要先接收 $c,然后是 $a。
$f = complex(b: 2, d: 4, c: ?, a: ?);

// 保持所有参数未绑定。嘿,看,这是第一类可调用对象!
$f = complex(...);

// 这会创建一个零参数闭包,它在被调用时直接调用 complex()!
$f = complex(1, 2, 3, 4, ...);

PFA 支持各种复杂的使用场景和功能,如参数重排序、命名参数、可变参数等。然而在实践中,我们预计大多数用途是将函数减少到仅剩一个参数(即柯里化)。这使得它非常适合作为回调函数使用。PHP 中大多数接受回调的函数都期望单个参数,少数接受两个参数(例如值和键)。PFA 使得在这些情况下使用任意复杂且上下文感知的函数变得极其简单。

// 这
$result = array_map(in_array(?, $legal, strict: true), $input);

// 比下面这个要好得多
$result = array_map(fn(string $s): bool => in_array($s, $legal, strict: true), $input);

从设计上看,它也是新 管道运算符 的完美补充。复用 Pipe RFC 中的一些示例:

$numberOfAdmins = getUsers()
    |> array_filter(?, isAdmin(...))
    |> count(...);

$result = "Hello World"
    |> htmlentities(...)
    |> str_split(...)
    |> array_map(strtoupper(...), ?)
    |> array_filter(?, fn($v) => $v != 'O')
;

更重要的是,围绕管道运算符的优化意味着在这些情况下甚至不需要创建闭包,因此没有任何性能开销。

RFC 文档提供了关于新语法的所有细节。

长远展望

如果这一切听起来像是“第一类可调用对象”的扩展版本,那确实如此。或者说,“第一类可调用对象”是部分函数应用的训练轮版本。

PFA 早在 2021 年就被 首次提出,由 Joe Watkins、Levi Morrison、Paul Crovella 和我组成的团队提出。那个版本在表面上大致相似,但实现方式不同,导致了一些争议。特别是 Nikita Popov(当时仍是 PHP 的事实上的首席开发者)认为它在引擎中引入了过多的复杂性。他的犹豫说服了当时许多人拒绝它,尽管当时仍有大量兴趣和支持。

然而,支持度足够高,以至于 Nikita 问道:“我们难道不能直接用 foo(...) 来延迟所有变量,并跳过 RFC 的其他部分吗?”结果就是 第一类可调用对象语法 RFC,于 PHP 8.1 中发布。

从那以后我一直在寻找机会再次尝试 PFA,但需要合适的时间和合适的合作伙伴。FCC 显然已被证明是对语言的巨大助力,为什么不彻底一点呢?直到今年早些时候 Pipes RFC 通过,我才成功邀请到 PHP Foundation 的 Arnaud Le Blanc 与我一起开发第二个版本。由于时间原因,它未能进入 PHP 8.5,但现在已在 8.6 中可用。

那么,什么发生了变化?首先,FCC 最终已经包含了此版本 PFA 所需的大量底层引擎技巧。我们可以利用这一点。其次,实现方式略有不同。与其创建一种可以额外优化的特殊伪闭包,新方法只是像多年来一样创建一个普通的闭包对象。这使得实现更加简单,并能解决大量边缘情况的问题。第三,现在我们有了管道。

而且哦,这是一个令人兴奋的组合。

迟来的功能

PHP 的 PFA 实际上早在 2021 年之前就已经开始酝酿。正如 7 月的 Pipes 博客文章 中所讨论的,早在 2016 年 Sara Golemon 就提议将 Hack/HHVM 的管道语法移植到 PHP:

$result = $arr
    |> array_column($$, 'tags')
    |> array_merge(...$$)
    |> array_unique($$)
    |> array_values($$)
;

那从未获得批准,但它促使我们将语法分为两部分:管道运算符本身,以及用部分函数应用代替 $$。我们在 2021 年尝试同时获得两者,但都失败了。现在两者都有了。

我见过关于新管道运算符的主要批评之一是,需要将多参数函数包装成内联箭头函数,然后将其包裹在 () 中以让解析器满意。这是一个合理的批评!而针对这一批评的完美解决方案正是……部分函数应用。我们现在拥有了它。这对双胞胎终于团聚了。

PHP 打破常规

我经常看到有人批评 PHP 只是从其他语言窃取功能并随意堆砌。坦率地说,这并不总是坏事:PHP 就像英语一样,通过在其他语言中寻找好主意并咳咳借用它们,使其成为我们自己的东西而发展。

部分函数应用并非新概念。几十年来,它一直是许多函数式语言的基础。例如,Haskell 隐式地为每个函数调用使用部分应用。任何函数调用都可以省略其最右侧的参数,然后它就变成了部分应用。

然而,我在任何语言中都没有看到过能够部分应用任意参数的能力。这对 PHP 很重要,因为虽然 Haskell 的整个标准库都是基于最右侧部分应用的假设构建的,但 PHP 绝对不是这样。我们需要能够将任意函数转换为一元(单参数)函数,以便标准库的大多数部分能够与……管道配合工作。或者作为回调函数。

而现在我们可以了。我不知道有任何其他语言拥有像 PHP 8.6 那样灵活、强大且紧凑的部分函数应用语法。在这里,PHP 似乎成为了创新者。

摇滚吧,ElePHPants!

接下来是什么?

还有一块重要的拼图尚未完成:函数组合。其中管道立即执行,而函数组合通过将两个函数首尾相连来创建新函数。Sara Golemon 帮助启动了这项工作,但在正式提出之前仍需要一些工作。

这将完成我们多年来一直试图引入 PHP 的“函数式特性”三重奏,从而允许更自然地使用函数式技术。

这些 RFC 中的每一个单独来看都是有用的,但不足以惊天动地。放在一起……“协同效应”可能在管理咨询之外是一个贬义词,但在这种情况下它适用。我们非常接近彻底打开 PHP 的函数式能力,就像 PHP 5.2 最终打开了其面向对象的能力一样。作为一个多范式语言,我们将能够自由地混合搭配 OOP 和 FP 方法,在它们最合适的时候使用。