🗒️PHP 8.5 管道运算符:小特性,大潜力

发布于2021-07-02
PHP 8.5 将于 2025 年 11 月发布,此次更新带来了一个期待已久的特性——管道运算符(|>)。这个看似简单的小特性,却有着巨大的潜力,不过它的实现过程却耗费了数年时间。

什么是管道运算符?

管道运算符写作|>,看似简单。它会获取左侧的值,并将其作为唯一参数传递给右侧的函数(在 PHP 中为callable),例如:

plain

$result = "Hello World" |> strlen(...) // 等同于 $result = strlen("Hello World");
Plain text
其真正有趣之处在于,当它被重复使用或串联起来形成一条“管道”时。比如在一个实际项目中,原本的代码经过改写使用管道操作后,变得更加清晰易读:

plain

$arr = [ new Widget(tags: ['a', 'b', 'c']), new Widget(tags: ['c', 'd', 'e']), new Widget(tags: ['x', 'y', 'a']), ]; $result = $arr |> fn($x) => array_column($x, 'tags') // 获取数组的数组 |> fn($x) => array_merge(...$x) // 扁平化为一个大数组 |> array_unique(...) // 去除重复项 |> array_values(...) // 重新索引数组 ; // $result 为 ['a', 'b', 'c', 'd', 'e', 'x', 'y']
Plain text
如果不使用管道运算符,实现相同功能的代码要么是嵌套丑陋的:

plain

array_values(array_unique(array_merge(...array_column($arr, 'tags'))));
Plain text
要么需要为每一步创建临时变量,这会带来额外的思维负担,而且这样的链式操作无法在match()块等单表达式上下文中使用,而管道链则可以。
熟悉 Unix/Linux 命令行的人可能会发现它与 shell 管道|相似,这并非偶然,它们的作用本质上是一样的:将左侧的输出作为右侧的输入。

管道运算符的由来

|>运算符在许多语言中都有出现,主要是在函数式编程语言中。F# 和 OCaml 拥有基本相同的运算符,Elixir 则有一个稍复杂的版本(我们曾考虑过,但目前最终决定不采用)。实际上,有许多 PHP 库也提供了类似的功能,只是步骤更为繁琐,包括作者自己的 Crell/fp
而 PHP 管道的故事始于 Hack/HHVM,它是 Facebook 开发的 PHP 分支。Hack 包含了许多当时 PHP 5 所不具备的特性,其中很多最终被纳入了后续的 PHP 版本中,管道运算符便是其中之一,不过它有其独特之处。
2016 年,长期的 PHP 贡献者、HHVM 项目前开源负责人 Sara Golemon 提议将 Hack 的管道 直接移植到 PHP 中。在该 RFC 中,管道的右侧不是callable,而是一个表达式,并使用一个特殊的$$标记(亲切地称为T_BLING)将左侧的结果注入其中。按照这种方式,上面的示例代码会是这样:

plain

$result = $arr |> array_column($$, 'tags') |> array_merge(...$$) |> array_unique($$) |> array_values($$) ;
Plain text
这种方式虽然强大,但也有一定的局限性。它非常不标准,与其他任何语言都不同,而且还意味着一种奇怪的、一次性的部分调用函数的语法,这种语法仅在与管道结合使用时才有效。
该 RFC 并未进行投票。此后几年没有太多进展,直到 2020/2021 年。当时作者刚写完一本关于 PHP 函数式编程的书,其中谈到了函数组合,于是决定尝试实现管道运算符。特别是,作者与一个团队合作,将 部分函数应用(PFA)作为一个独立的 RFC,与更 传统的管道 分开。其想法是,将一个多参数函数(如上面的array_column())转换为|>所需的单参数函数,这本身就是一个有用的特性,并且应该可以在其他地方使用。其语法与 Hack 版本有所不同,为了使其更灵活:some_function(?, 5, ?, 3, ...),这会将一个 5 个或更多参数的函数转换为一个 3 个参数的函数。
遗憾的是,由于一些引擎复杂性问题,PFA 没有通过,这在很大程度上也削弱了第二个管道 RFC。不过,我们也从中获得了一个安慰奖:由 Nikita Popov 提出的 一等可调用对象array_values(...)语法),本质上是部分函数应用的一个“初级”、简化版本。
时间快进到 2025 年,作者再次尝试实现管道运算符。这一次,在 PHP 基金会开发团队的 Ilija Tovilo 和 Arnaud Le Blanc 的大力帮助下,作者成功实现了它。真可谓事不过三。

大于各部分之和

如前所述,管道运算符看似简单。其实现本身几乎微不足道,实际上它只是临时变量版本的语法糖。然而,最好的特性是那些可以与其他特性结合使用,或者以新颖的方式使用以发挥更大作用的特性。
我们已经看到,一个漫长的数组操作过程现在可以浓缩为一个单一的链式表达式。现在想象一下,在只允许单一表达式的地方使用它,例如match()

plain

$string = 'something GoesHERE'; $newString = match ($format) { 'snake_case' => $string |> splitString(...) |> fn($x) => implode('_', $x) |> strtolower(...), 'lowerCamel' => $string |> splitString(...), |> fn($x) => array_map(ucfirst(...), $x) |> fn($x) => implode('', $x) |> lcfirst(...), // 其他情况选项 };
Plain text
此外,右侧也可以是一个返回Closure的函数调用。这意味着通过一些返回函数的函数:

plain

$profit = [1, 4, 5] |> loadSeveral(...) |> filter(isOnSale(...)) |> map(sellWidget(...)) |> array_sum(...) ;
Plain text
这几乎与长期讨论的标量方法相同!只是管道更加灵活,因为你可以在右侧使用任何函数,而不仅仅是那些被语言设计者视为方法的函数。
在这一点上,管道非常接近“扩展函数”,这是 Kotlin 和 C# 的一个特性,允许编写看起来像对象上的方法但实际上是独立函数的函数。它们的写法略有不同(这里用|而不是-),但已经实现了 75% 的功能,而且是免费的。
再进一步思考,如果管道中的某些步骤可能返回null怎么办?我们可以通过一个单一的函数“提升”我们链中的元素,以与空安全方法相同的方式处理null值。

plain

function maybe(\\Closure $c): \\Closure { return fn(mixed $arg) => $arg === null ? null : $c($arg); } $profit = [1, 4, 5] |> maybe(loadSeveral(...)) |> maybe(filter(isOnSale(...))) |> maybe(map(sellWidget(...))) |> maybe(array_sum(...)) ;
Plain text
没错,我们只是用一个管道和一个单行函数实现了一个 Maybe 单子。
现在,考虑一下流的情况:

plain

fopen('pipes.md', 'rb') // 没有变量,所以当垃圾回收时它会自动关闭 |> decode_rot13(...) |> lines_from_charstream(...) |> map(str_getcsv(...)) |> map(Product::create(...)) |> map($repo->save(...)) ;
Plain text
其潜力是巨大的。可以毫不夸张地说,管道运算符是近年来“性价比”最高的特性之一,与构造函数属性提升等优秀特性不相上下。而这一切都要归功于这一点点语法糖。

接下来会发生什么?

尽管管道是一个重要的里程碑,但我们的工作还没有完成。目前正在积极推进不止一个后续 RFC。
第一个是再次尝试 部分函数应用。这是一个更大的特性,但一等可调用对象已经带来了许多必要的基础结构,这简化了实现。由于管道现在提供了一个自然的用例以及容易优化的点,值得再次尝试。在撰写本文时,它是否会纳入 PHP 8.5、推迟到 8.6 还是再次被拒绝还不确定,但作者对此充满希望。特别感谢 PHP 基金会团队的 Arnaud Le Blanc 接手并更新该实现。
第二个是 函数组合运算符。管道会立即执行,而函数组合则通过将两个函数首尾相连来创建一个新函数。这意味着上面的流示例可以通过组合map()调用来进一步优化:

plain

fopen('pipes.md', 'rb') |> decode_rot13(...) |> lines_from_charstream(...) |> map(str_getcsv(...) + Product::create(...) + $repo->save(...)) ;
Plain text
这个特性肯定不会纳入 PHP 8.5,但作者希望能将其纳入 8.6。请继续关注。
特别感谢 PHP 基金会团队的 Ilija Tovilo 和 Arnaud Le Blanc 对管道实现的帮助。如果你想帮助推动 PHP 的发展,可以考虑 成为赞助商
 
使用 Asyncpg 和 PostgreSQL 提升应用性能基于JWT的用户认证
Loading...
©2021-2025 Arterning.
All rights reserved.