PHP 8.5 引入管道运算符:我们用它可以做什么?
将于今年11月发布的 PHP 8.5 将带来另一个备受期待的功能:管道运算符(|>
)。这是一个看似简单却潜力巨大的功能,但其实现仍耗时数年。
什么是管道运算符?
管道运算符(|>
)看似简单,其实大有文章。它将左侧的值作为唯一参数传递给右侧的函数(在 PHP 中为 callable
):
$result = "Hello World" |> strlen(...)
// Is equivalent to
$result = strlen("Hello World");
单独来看,这并不算特别有趣。但当它被重复使用或链式调用以形成“管道”时,便变得有趣起来。例如,以下是我参与的真实项目中的真实代码,经过重构以使用管道:

$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') // Gets an array of arrays
|> fn($x) => array_merge(...$x) // Flatten into one big array
|> array_unique(...) // Remove duplicates
|> array_values(...) // Reindex the array.
;
// $result is ['a', 'b', 'c', 'd', 'e', 'x', 'y']
若不使用管道,相同代码将需要以下令人作呕的嵌套结构:
array_values(array_unique(array_merge(...array_column($arr, 'tags'))));
或者手动为每个步骤创建临时变量。虽然临时变量并非世界末日,但它们会增加额外的认知负担,且意味着这样的链式结构无法在单表达式上下文中使用,例如 match()
块。而管道链式结构可以。
任何在 Unix/Linux 命令行上工作过的人都可能注意到它与 shell 管道 |
的相似性。这绝非偶然,因为本质上它们是同一件事:将左侧的输出作为右侧的输入。
它从何而来?
|>
运算符在许多语言中都有出现,主要集中在函数式编程领域。F# 拥有几乎完全相同的运算符,OCaml 也是如此。Elixir 则有一个稍显花哨的版本(我们曾考虑过但最终决定暂不采用)。在实际应用中,存在许多 PHP 库提供了类似功能,但往往需要额外复杂的步骤,包括我自己的 Crell/fp。
然而,PHP 管道的故事始于 Hack/HHVM,即 Facebook 的 PHP 分支(最初作为竞争性实现)。Hack 包含了许多超越当时 PHP 5 版本的功能;其中许多最终被纳入了后续的 PHP 版本。其中一项特色是管道操作符的独特实现。
2016年,长期参与PHP开发的Sara Golemon(曾担任HHVM项目开源负责人)提议将Hack的管道直接移植到PHP中。在该 RFC 中,管道的右侧并非 callable
,而是一个表达式,并使用了一个魔法 $$
令牌(亲切地称为 T_BLING
,至少根据我个人的说法)将左侧的结果注入其中。在这种情况下,上面的示例将看起来像这样:
$result = $arr
|> array_column($$, 'tags')
|> array_merge(...$$)
|> array_unique($$)
|> array_values($$)
;
虽然强大,但它也有些局限性。它非常非标准,与其他任何语言都不一样。它还意味着一种奇怪的、一次性语法,用于部分调用函数,而这种语法仅在与管道配合使用时才有效。
该 RFC 并未进行投票表决。此后数年间鲜有进展,直至 2020/2021 年。那时,我刚完成一本关于 PHP 函数式编程的书籍(书中探讨了函数组合),决定尝试解决这一问题。具体来说,我与一个团队合作,将部分函数应用 (PFA)作为一个独立的RFC,与更传统的管道区分开来。该想法是,将多参数函数(如上文的array_column()
)转换为|>
所需的单参数函数本身就是一个有用功能,且应可在其他场景中复用。该语法与 Hack 版本略有不同,以增强灵活性:some_function(?, 5, ?, 3, ...)
,该语法可将一个接受 5 个或更多参数的函数转换为一个接受 3 个参数的函数。
遗憾的是,由于引擎复杂性问题,PFA 未获通过,这在很大程度上也削弱了 v2 管道 RFC 的可行性。然而,我们从中获得了一点安慰:第一类可调用对象(即 array_values(...)
语法),由 Nikita Popov 提供,其设计初衷就是部分函数应用的“初级”退化版本。
快进到 2025 年,我感到足够无聊,决定再次尝试管道。这次有了 Ilija Tovilo 和 Arnaud Le Blanc 的大力协助(两人均为 PHP 基金会开发团队成员),我终于成功通过了审核。
第三次尝试终于成功。
整体大于部分之和
如上所述,管道被描述为“看似简单”。其实际实现几乎微不足道,本质上只是临时变量版本的语法糖。然而,最强大的特性在于它们能够与其他特性结合或以创新方式使用,从而发挥超出自身能力范围的作用。
我们之前看到,一个冗长的数组操作过程现在可以浓缩为一个单一的链式表达式。现在想象一下在只允许单一表达式的地方使用它,例如 match()
:
$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(...),
// Other case options here.
};
或者,考虑右侧也可以是一个返回 Closure
的函数调用。这意味着通过几个返回函数的函数:
$profit = [1, 4, 5]
|> loadSeveral(...)
|> filter(isOnSale(...))
|> map(sellWidget(...))
|> array_sum(...);
这与我们长期讨论的标量方法几乎相同!只是管道更灵活,因为你可以使用右侧的任何函数,而不仅仅是那些被语言设计者认可为方法的函数。
此时,管道已经非常接近“扩展函数”这一特性,这是Kotlin和C#中的一项功能,允许编写看起来像对象上的方法,但实际上只是独立函数的函数。拼写稍有不同(|
而不是 -
),但它已经完成了 75% 的工作,而且是免费的。
或者更进一步。如果管道中的某些步骤可能返回 null
呢?我们可以使用一个函数,将管道中的元素“提升”到与 null 安全方法相同的方式处理 null
值。
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(...));
没错,我们刚刚用管道和一个单行函数实现了 Maybe 单子。
现在,想想这对流处理意味着什么……
fopen('pipes.md', 'rb') // No variable, so it will close automatically when GCed.
|> decode_rot13(...)
|> lines_from_charstream(...)
|> map(str_getcsv(...))
|> map(Product::create(...))
|> map($repo->save(...))
;
潜力绝对巨大。我认为说管道操作符是近年来最具“性价比”的特性之一并不为过,与构造函数属性提升等特性并驾齐驱。而这一切都得益于一点语法糖。
接下来会发生什么?
尽管管道是一个重大里程碑,但我们尚未完成。目前正在积极推进两个后续 RFC 的工作。
第一个是关于部分函数应用的第二次尝试。这是一个较大的功能,但由于一等可调用对象已经引入了大部分必要的底层实现,这简化了实现过程。由于管道现在提供了自然的使用场景,以及易于优化的点,值得再次尝试。它是否会纳入PHP 8.5、推迟到8.6,还是再次被拒绝,目前尚不确定,但我对此抱有希望。特别感谢PHP基金会团队的Arnaud Le Blanc接手并更新了实现。
第二个是函数组合运算符。与管道运算符立即执行不同,函数组合运算符通过将两个函数首尾相接来创建一个新函数。这意味着上述流处理示例可通过合并 map()
调用进一步优化:
fopen('pipes.md', 'rb')
|> decode_rot13(...)
|> lines_from_charstream(...)
|> map(str_getcsv(...) + Product::create(...) + $repo->save(...))
;
该提案肯定不会纳入 PHP 8.5,但我对它能进入 8.6 版本抱有希望。敬请期待。
我第一次看到管道运算符 |> 在实际应用中使用是在 F# 语言中。你可以这样写:
它能正常工作是因为 |> 会将左侧表达式的输出作为最后一个参数传递给右侧函数。multiply 必须定义为:
这样 b 就会变成 3,而 c 则接收 sum 1 2 的结果。
右侧也可以是一个lambda表达式:
|> 并非语法糖,而是标准库中定义的函数:
对于函数组合,F# 提供了 >>(正向组合)和 <<(反向组合),分别定义为:
我们可以使用它们来构建可重用的组合函数:
F# 是一门美丽的语言。遗憾的是,微软早已停止对这门语言的投资,而且对(类型化)函数式编程语言的兴趣也不高。
F# 非常出色。但其工具链、生态系统和编译速度是我不使用它的原因。我曾与 OCaml 一起学习,而 OCaml 的编译速度让我 spoiled。
确实遗憾的是,F# 从未成为一等公民。
近年来,尤其是工具链和生态系统方面,情况有了显著改善。
OCaml 是一门伟大的语言,ML 家族中的其他语言也是如此。我认为 Isabelle 是第一门引入 |> 管道字符的语言。
还有 ||> 和 |||> 用于自动解构元组并将每个部分作为单独的值传递。
还有反向管道(<|、<|| 和 <|||)
对我来说,F# 是最符合人体工程学的语言。但微软没有投资于它,因此在行业中实际使用 F# 的机会也非常少。
PHP 在 JS 完成其版本之前就获得了管道操作符,这有点令人惊讶……当然,他们提出了多个竞争方案,其中我最喜欢的是受 F# 启发的那个……我已经不再依赖 Babel(太过臃肿)之类的工具,否则我希望能使用它。我多年来一直使用它来实现异步函数和模块语法,直到这些功能在 Node 和浏览器中被实现。现在很难再为它辩护。
|> 在 F# 中真的是一个运算符吗?我认为它只是标准库中的一个普通函数,但也许我记错了。
它在标准库中定义,任何人都可以重新定义它。
它通常被称为运算符,因为它使用了中缀表示法。
F# 中所有运算符都是函数,例如以下代码是有效的:(+) 1 2
Haskell似乎也相当衰落。好在PHP还有其他选项来处理行噪声。
是什么让你认为Haskell已经死亡或正在衰落?GHC的新版本正在发布,根据我的经验,开发Haskell从未如此顺畅(这并非意味着完全没有问题)。
比较2020年和2025年的Redmonk排名:
https://redmonk.com/sogrady/2020/02/28/language-rankings-1-2…
https://redmonk.com/sogrady/2025/06/18/language-rankings-1-2…
我认为编程语言大致可分为三类:
占主导地位的保守选择。这些是无需向CTO解释的语言,就像“选择IBM不会被解雇”一样。例如Java、Python等。
知名但需要权衡的选择。这些语言拥有足够的生态系统和技术积累,可以为选择它们提供合理依据,但选择时仍需权衡利弊和风险。或是那些在某个领域占据主导地位但在其他领域表现平平的语言。例如Ruby、Scala、Swift、Kotlin。
其他所有语言。这些是你在职业场合使用时需要争取的语言。它们要么是新颖创新的,要么是老旧衰落的。
2020年,Haskell与Kotlin、Rust、Dart处于同一水平,它们属于第三类,但其发展趋势指向第二类。2025年,Kotlin和Dart已领先进入第二类,但Haskell却朝相反方向发展。它落后于Perl,而Perl本身也并不景气。
这并非意味着Haskell是糟糕的语言。许多优秀语言并未广泛使用。流行度难以捉摸,更多取决于语言本身的优点之外的外部因素。否则 JavaScript 就不会位居榜首。
> 2020 年,Haskell 与 Kotlin、Rust 和 Dart 处于同一水平。[…] 2025 年,Kotlin 和 Dart 已领先进入第二梯队,但 Haskell 却在朝相反方向发展。
> 它落后于 Perl,而 Perl 本身也并不算表现出色。
你的评论让我想起了那些通过观看 YouTube 上的“Let's Play”视频来“玩游戏”的玩家。
然而,尽管PHP、Java,甚至像Kotlin、Clojure或Scala这类更小众/更新的语言都有大量杀手级软件(即那些值得学习一门语言只为使用其库/框架的软件),Haskell在30年后却毫无建树。零。
请注意,我了解并喜欢Haskell,但其问题与“简单Haskell计划”的失败(以及其工具链的糟糕状态)密切相关。
有很多优秀的库,如repa、servant、megaparsec、gloss、yampa……以及对许多标准库的绑定。我认为解析是Haskell的杀手级优势之一,我一定会用它来编写编译器。
还有一些流行的面向用户的软件,如用Haskell编写的Pandoc。以及内部使用它的公司。
用Haskell编写的唯一一个非无关紧要的编译器是为另一个濒临死亡的项目Elm编写的。
你在说什么?
Agda编译器、Pugs、Cryptol、Idris、Copilot(不是你想到的那个Copilot)、GHC、PureScript、Elm……
这些可能不是主流,但在其细分领域中(或曾是,但其他的是当前的)具有重要性。
它有PostgREST,这是supabase的核心
我绝不接受对 Xmonad 的诽谤
是的,我同意。Haskell 曾是我在 00 年代的主要编程语言。但它在行业中的采用率几乎为零。别拿 Jane Street 或某个初创公司来跟我说。
我曾以为自己能专注于寻找喜欢 Haskell 的工作。但这从未发生。
我当然不会刻意追求Haskell相关的工作。但这类岗位确实存在;例如我当前的工作就是Haskell项目,且与我上一份工作(主要使用Scala)属于同一行业(公共交通)。
此外,多年来我发现Haskell适用于某些一次性任务,例如
– 从一个巨大的XML文件中提取大量交叉引用数据。我尝试了我们的一些“常用”语言/系统,但它们都耗尽了内存。Haskell让我快速编写了一个足够高效的解决方案。不确定此后是否还使用过(如果使用过,那肯定属于技术债务)。
– 测试新系统是否与被替换系统的行为一致。这是一个单人任务,旧系统被替换后该代码就被弃用了;因此不存在技术债务。事实上,这是在一家PHP公司完成的 🙂
当然,这也不是我的重点。我的意思是,Haskell在研究生阶段看起来多么出色,与现实世界的兴趣并不匹配。
我现在使用Spark完成大多数此类任务。Guido从Haskell借鉴了足够多的内容,使得pyspark对于许多此类任务实际上非常吸引人。
> Guido从Haskell借鉴了足够多的内容,使得pyspark对于许多此类任务实际上非常吸引人。
他没有做好功课。Guido或目前负责Python语言委员会的人缺乏足够的思维能力,无法意识到
match
必须是一个可绑定的变量表达式,而绝不能是一个语句,以防止类型分支。他们也拒绝承认,套接字上的非阻塞描述符必须是运行时的默认属性,而绝不能通过语言语法进行赋值,尽管就连Java开发者都通过示例证明了这一点。> 还有其工具链的糟糕状态
这是毫无根据的恐吓宣传
> Haskell在30年后仍没有
> 我了解Haskell
我表示怀疑
顺便说一下,instig007 并非 Haskell 社区的一员,但似乎偶尔会对 Haskell 进行批评,这可能给 Haskell 社区带来不良影响:https://news.ycombinator.com/item?id=44199980
学习和使用拉丁语也非常容易。
是的,但现代拉丁语俚语非常少。而GHC经常为我们提供Haskell的出色新扩展。
拉丁语从未帮我付过房贷。不过在SAT考试中倒是帮了点忙。
与此同时,JS世界已经等待了10年,这个提案仍处于第2阶段https://github.com/tc39/proposal-pipeline-operator/issues/23…
我们不仅等待了10年,而且最有可能被采纳的方案与提案最初设想的完全不同:
我们希望有一个管道操作符,能够与一元函数(如通过部分函数应用创建的函数,这类函数可以拥有自己的语法)良好配合,但该提案因被认为会导致使用过多闭包的编程风格[0], 并可能分裂生态系统[1]而被否决。
然而,PHP 并未受这些假设的限制,而是直接为用户提供了他们想要的功能,并且以最合理的方式呈现。
[0]: https://github.com/tc39/proposal-pipeline-operator/issues/22…[1]: https://github.com/tc39/proposal-pipeline-operator/issues/23…
我理解正确吗?您是在说,最广泛使用的 JS 引擎的开发者表示“我们看不到在不牺牲性能的情况下实现这一点的方法”,这是一个荒谬的假设,应该被忽略?
由于JS的async/await系统本质上是通过创建临时闭包来运行的,我认为情况不会有太大变化,说实话。
此外,我不明白为什么引擎应该监管什么是或不是可接受的性能。在大多数情况下,使用函数接口(map/forEach等)比使用for循环更慢,但这并没有阻止它们实现这些接口。
我认为在比较以下两段代码时,性能影响并不大
和
尤其是在使用现有语言特性时,你可能会写出类似的代码:
。
他们在链接的GitHub问题中讨论的问题是管道中函数接收多个参数的情况。
在这种情况下,管道版本会创建大量一次性闭包。
他们无法在不牺牲性能的情况下实现函数应用?我对此表示怀疑。尤其是考虑到函数应用已经是语言中常见的(甚至可以说:必不可少)功能,例如:`Math.sqrt(2)`。
我们所要求的只是能够将它重写为`2 |> Math.sqrt`。
据我所知,他们担心的是,人们_假设性地_可能会更加依赖闭包,而闭包本身的性能不如类。
然而,我认为引擎实现者不应过多关注人们如何编写代码。人们总是可以编写低效的代码,而这属于他们自己的责任。所以我不知道这是否“愚蠢”,但我不同意这种观点。
除非我理解有误,或者以稍有不同的方式进行函数应用实际上是一个非常困难的问题。谁知道呢。
你的简单示例(
2 |> Math.sqrt
)看起来很棒,但当代码变得更复杂时,管道语法的优势就不再那么明显了。例如,会变成类似以下形式:
或
这看起来只是 JavaScript 中另一种写法,并不更易于阅读。优势何在?
嗯,那别这么做。这就像认为三元运算符的引入是个糟糕的改动,因为并非所有 if/else 块在转换后都更美观。
目标是线性化一元函数应用,而不是让所有代码看起来更好。
我认为评论者意思是,一旦新语法被社区批准并采用,你就别无选择只能使用该语法。你最终会修改项目,并被迫处理审查这段代码的问题。
闭包在 JavaScript 中早已取代了面向对象编程(例如 React 从类切换到函数 + 闭包),但他们仍然试图强迫社区接受像私有变量这样的垃圾。
大量新增的 JavaScript 功能要么性能更差,要么在理论上可能导致性能下降,但这从未阻止过他们。
一些具体(不完全列举)的例子:
* 私有变量通常比非私有变量慢 30-50%(并且会破坏代理)。
* let/const 比 var 慢几个百分点。
* 生成器比循环慢。
* 迭代器通常因生成垃圾返回值而更慢。
* Rest/spread 运算符隐藏了你正在分配新数组和对象的事实。
* 代理会导致代码速度极度下降。
* 允许对内置函数进行子类化会让一切变慢。
* BigInt 作为设计几乎总是比引擎推断的 31 位整数更慢。
与此同时,谷歌和Mozilla拒绝实现正确的尾调用,尽管这会提升大量代码的性能。他们取消了SIMD项目(尽管这些项目已经实现),这进一步降低了对性能要求最高的应用程序的性能。
显然,当他们想添加某项功能时,性能就不是问题;而当他们不想添加时,性能就成了一个方便的借口。
我真希望我能多次为这个提案点赞。我非常喜欢受F#启发的管道操作符提案,甚至在过去依赖6to4/babel时也使用过它,但它似乎一直被搁置。我实在想不出还有哪个语言特性是我更想要的。新的Temporal是例外。
似乎所有真正优秀的语言提案都未能获得足够关注。尾调用(Proper Tail Calls)已在语言中存在十年之久,但V8和Spidermonkey仍违反规范且无故拒绝实现。
记录/元组被取消,尽管它是消除隐藏类变异、提供深度O(1)比较以及使WebWorkers/线程/演员值得使用的最佳提案,因为数据传输不会成为瓶颈。
模式匹配、do 表达式、for/while/if/else 表达式、二进制 AST 等特性多年来一直被搁置,而规范委员会似乎并不关心这些特性对开发者和/或用户而言具有实际、可衡量的优势,且不会为 JIT 带来太多复杂性。
我确信,委员会的大多数成员与实际每天使用 JS 的人完全脱节。
我认为主要是因为他们在管道的语法选择上难以达成共识,因为人们分为三个不同的阵营。我希望他们能将这三个选项标准化,每个选项使用略微不同的运算符。
> * 展开运算符隐藏了你正在分配新数组和对象的事实。
仅在函数调用中,对吧?如果你在[]或{}中使用展开,你已经知道它会分配。
过去常说“Lisp程序员知道一切的价值,却不知其成本”。
以我经验,这适用于当今大多数开发者,尤其是JS和Python开发者,这主要归因于教育不足。我对从未上过大学的开发者没有意见,但问题在于他们连自学都不愿尝试。
我曾与许多JS开发者合作,他们对系统的工作原理一无所知。内存分配和垃圾回收对他们来说是纯粹的魔法。他们也不了解指针或栈与堆的区别。他们只知道这是让代码运行的魔法。对于这类开发者来说,他们只是创建所需的对象,却不明白这会对性能产生影响。
即使在知识丰富的开发者中,你经常会听到“它已经足够快了”的论调,或许还会提到“如果需要的话,以后再优化”。结果就是一种“由成千上万次小分配导致的缓慢”现象,整个应用程序运行得比预期更慢,且没有明显的性能瓶颈,因为整个代码库就是一个巨大的、未优化的代码团块。
归根结底,易用性、开发者无知和截止日期压力意味着性能几乎总是最后的优先级。
我知道这是 Records/Tuples 提案被否决的原因。我没有深入研究管道操作符,只是对两个提案有了大致了解。
如今,大多数更有趣的提案都处于停滞状态。当你看看所有进展到第3-4阶段的提案时,你会想:“好吧,我确信这对于某个我甚至不使用的功能来说有惊人的性能提升……但我真的在乎吗?”
我猜这部分是我的错,但即使在文章中,你也能看出Hack语法比函数式语法更易于使用。
另一个角度是“更改需要多少重写”,例如,如果我想在右侧函数调用中添加另一个参数。(我显然不认为柯里化或无点风格是好的解决方案)
我好奇PHP明确拒绝Hack风格的管道(尤其是考虑到PHP与Hack的密切关系,以及PHP没有部分应用,而JS有,虽然其用户体验可以改进)是否会为F#风格的提案增添优势,使其优于Hack风格。
这可能是 TC-29 提案支持者可以用来为 F# 风格辩护的有用的数据。
这真的没有必要,只是语法糖。使用点运算符几乎可以实现相同的效果。PHP 没有链式调用。添加更多的复杂性并不会让语言变得更好。
我已经听腻了关于 JavaScript 每新增一个语法特性时,总有人重复同样的论调:“没必要”、“只是语法糖”、“过于复杂”。然而,一旦这些特性被纳入语言,人们的反对声便会逐渐平息,很快大家就开始使用它们,最终这些特性也变得无争议。
如果有人真的认为这个新语法会让在 JavaScript 中编码变得更难,那就提供一些证据。进行一项研究,比较在语言的两个版本中(一个包含这个功能,一个不包含)解决代表性任务的情况,证明它对代码质量和可读性有负面影响。
显然,提出更改的人应该提供这样的研究,证明相反的情况。
其实没什么必要,C89已经足够好了。
点号和链式调用不一样,没人想用像underscore/lodash那样允许链式调用的方式,因为这会让死代码消除变得不可能。
K&R C对UNIX System V来说已经足够好了,为什么要用C89。
K&R C是巅峰,此后我们一直在走下坡路。
这点我完全同意,但不是开玩笑。
链式调用需要创建一个类,并确保所有内容都遵循该类并正确返回,以免链式调用出错。随着添加更多选项和功能,这变得越来越难以编写和维护。
如果我使用一个链式库并需要另一个方法,我必须理解底层数据模型(一个泄露的抽象)并必须以某种hack式的方式扩展模型。由于我不是维护者,我可能会在过程中造成一些微妙的破坏。
管道运算符不存在这些问题。它们直观易懂。它们无需跟踪前一个运算符之后的状态(这也有助于调试)。如果需要扩展,只需查看响应值并添加相应的函数。
组合(无论是否使用管道运算符)都远优于链式调用。
当你说链式调用时,是指自动装箱基本类型吗?PHP 确实可以实现类似 `foo()->bar()?->baz()` 的操作,但你需要手动将数组/字符串包装起来,而不是从 `原型` 中提取方法来使用它。
> 使用点表示法几乎可以实现相同的效果。
关键词:几乎。管道不需要你在每种可能的类型上都拥有许多不同的方法:https://news.ycombinator.com/item?id=44794656
如果你的团队不希望使用这个新选项功能,只需在 CI/CD 管道中启用一个 PHPStan 规则,以防止此类代码被合并。
这其实不是链式调用
更像是 thenables / promises
看起来像链式调用,但可以添加自定义函数?
这是无需更改每个函数返回值的链式调用。在 JS 中无法直接调用 3.myMethod(),但可以通过 3 |> myMethod 实现
需要使用括号 `(3).myMethod()`,但可以通过修改 Number 原型实现。这是一个非常糟糕的主意,但确实可行。
不仅如此
在链式调用中,所有方法都必须属于同一个类。
在 C++ 中,我们很久以前就有这种东西,它被称为滥用流操作符,哈哈
点号用于调用对象上的函数,管道用于传递参数给函数。完全错过了重点。
不错——JS中管道的真实世界示例(https://github.com/tc39/proposal-pipeline-operator?tab=readm…)在我看来非常令人失望。
遗憾的是,他们过于执着于将管道与承诺结合,而承诺本身并不符合自然的流程。
去解释给他们听,承诺本身已经通过“then”方法拥有了自然的操作链式方式,无需强行将管道运算符套用到超出必要范围的场景。
在 TypeScript 中我们可以这样做
let res res = op1() res = op2(res.op1) res = op3(res.op2)
类型推断效果很好,而且非常容易调试和重构。在我看来,甚至比管道结果更好。
JavaScript 已经拥有足够的功能。
别让我开始谈论 Go 语言中的单子……
两者都在发展且非常流行
你将获得的最好结果是 3 个新的构建系统和 10 个新的框架
我在 PHP 社区讨论该功能时曾有过类似争论,但我认为语法阅读起来复杂得多,需要回溯才能理解。虽然可能更容易编写。
假设你正在扫描一段不熟悉的代码,试图识别其中的符号。理解输入和输出后,你会得到如下内容。
看看这个操作,想象你正在阅读一段自己没有编写的代码。这段代码嵌入在成百上千行代码之中。试着弄清楚这里的“result”是什么?你的眼睛会立即跳到最后一行去查看返回类型吗?
我最初的愿望是先大致了解$result是什么,然后再决定是否要深入研究它的推导过程。
它是一个字符串。不过,要弄清楚这一点,你必须跳到最后一行才能理解$result的类型。当你只是在理解代码时,重点在于目的地而非到达那里的路径,而理解这些需要你倒着读。
或许我有些老派,但变量通过自身定义来说明其作用或行为的自文档化特性,对编写可维护的代码和降低维护者的认知负荷至关重要。
中间赋值的问题在于它们会污染你的作用域。
你可能有 $values,然后将其转换为 $b、$values2、$foo、$whatever,你的代码必须永远警惕,确保它永远不会意外地引用 $values 或任何中间变量,因为它们只存在于生成某个下游结果的服务中。
有时,在允许你反复覆盖变量的语言中,这会稍好一些,`$values = xform1($values)`,但我们可以做得更好。
难以命名中间值只是问题的症状,因为许多中间值仅作为短暂的即时状态存在。
管道风格的代码是一种保持顶层代码整洁的通用方法。
如果PHP支持块作用域,这个问题会小得多,你可以将过程化代码用大括号包裹,然后大功告成
我在 Go 中经常使用这种方式,希望 PHP 也能支持。PHP 虽然允许这种代码块,但据我所知它们似乎只是空操作。
将代码放入函数中,你污染的作用域大小完全取决于你。
函数也会以相同方式污染作用域。你不希望被迫提取一个从未被重用的函数,仅仅为了隐藏中间值;你应该只在需要抽象时才提取函数。
管道转换专门允许你在每个短暂中间值的作用域内使用函数来清理代码。
你肯定希望将代码提取为函数,即使你不需要重用它。函数名称就是文档。这可以减轻阅读代码的人的心理负担。
这就是为什么我们有类和命名空间。
任何人都可以编写好代码或坏代码。避免使用新功能和语法并不会改变这一点。
我并不反对你的观点。我一开始也难以理解这些示例。但立即让我印象深刻的是,这种语法与返回值的对象方法链式调用几乎完全相同。
虽然这种语法不是我最喜欢的,但至少它在方法链式调用和现在函数链式调用(通过管道)之间保持了一致性。
说到查询构建器,我们不再需要猜测每次操作是否会修改底层查询对象或克隆它。我认为这是管道的另一个重大优势。
对我来说,这读起来很顺畅,因为我熟悉 Perl 的 map 和 jq 的 lambda。但我会更强烈地使用一个新的 `|=>` 运算符,暗示一个分布式 `|>` 到其现在推断且静默的 => 参数中:
教解析器将 `fn($x) |=> ELEM1, ELEM2` 分解为 `fn($x) => ELEM1 |> fn($x) => ELEM2 |> …`,这样用户就不用浪费时间重复输入,这正是我喜欢 Perl 的地方,而且这样做更加清晰明了——无需展开括号,就能清楚地看到操作的顺序,同时也不会干扰后续可能有不同需求的 |> 块。
当然,由于我来自 Perl,这使得使用 reduce 管道清理中间的数组汇总变得非常方便,然后用运算符替换所有单词,使其成为难以理解的乱码,但不再需要关心 $x:
这可以简洁地总结为一句易于理解的表达式,只要你知道 | 表示列,+ 表示合并,< 表示减少,并且 : 表示语法糖,用于将 fn($x) 的重复调用简化为 $x,同时保持一种稳定的语法,这种语法也可以被 reduce 函数利用:
这读起来像是一句简洁明了的句子,因为我从小就使用 Perl,它可以一目了然地被理解,因为它符合一目了然的标准!
因此,我不会必然实现这里所有可能的功能,因为 Perl 证明了愿意解析符号而非单词的人群并非完整的程序员群体。但我确实认为上述定义的类似开关的 |=> 非常有用 =)
这就是一个好的 IDE 所能提供的,它会显示 $result 的类型是字符串。
管道操作符(包括 T_BLING)是我在 Meta 上编写 Hack 时为数不多的享受之一。
> 这就是一个好的 IDE 所能提供的,它会显示 $result 的类型是字符串。
我认为父级指的是结果的含义,而非其类型。函数式编程有时会比传统的命令式风格更模糊含义。
如果你想要明确含义,就不要把变量命名为“result”
我并不反对你的观点,但我认为这个管道应该放在一个恰当命名的函数中(至少在 Elixir 中我是这么用的),以便更好地理解结果。
我认为这更多是习惯问题。这只是你不太熟悉的运算符和语法。就像如果英语中添加了一个你不熟悉的字符,并开始在你不认识的单词中使用它。
很多人对rest/spread语法也有同样的看法。
人们经常使用方法链,而且没有问题?这相当于类似于:
我认为这只是习惯问题。
你混淆了两个概念:习惯和简洁性。
我认为管道语法并不难读,但我更倾向于第一个方案。
无论如何,我们不应仅凭习惯来评判软件及其功能。
我完全同意使用中间变量(以及在类型化语言中显式类型注释)来提高代码可读性。
但也许管道语法可以改进为:
这与链式属性访问或方法调用,或更一般地说嵌套表达式并无二致。也就是说,如果你过度使用它,会降低可读性,但如果你为每个操作都命名结果,也会难以阅读,因为这会引入过多冗余信息。
> 我认为这种语法更难阅读,需要回溯才能理解。
与 `array_merge(…array_column($arr, ‘values’));` 或类似的嵌套函数调用相同。
> 想象你正在扫描一段不熟悉的代码,试图识别其中的符号。理解输入和输出,你会得到如下内容。
我们不需要想象 🙂 使用支持管道的语言的人每天都在查看类似的代码。
> 但通过几个变量定义事物是什么或在做什么的自文档化特性,对编写可维护的代码似乎很重要
管道不会阻止你使用几个变量。
在你的示例中,我需要跟踪 $values 变量,查看它在哪里被使用,展开嵌套函数调用等。
或者我只需查看顺序函数调用。
不过PHP本应做的只是将管道值作为任何函数的第一个参数传递。这样会干净得多:
我不会惊讶于最终会发生这种情况
这篇文章对此解释得相当到位。
简要总结:Hack 使用 $$(即 T_BLING)作为管道中的隐式参数。但这种用法并未像T_BLING这个名字听起来那么有趣。PHP团队开始寻找解决方案,并着手设计一种令他们满意的“部分函数应用”语法。这一努力大多陷入僵局(尽管他们希望未来能重新审视),唯一进展是为未应用的函数(即命名函数但不调用其方法)设计了some_function(…)这种语法。
看起来这是PHP函数不是第一类对象的一个有趣现象。希望他们能在进一步清理部分应用场景时好运。
我惊讶于示例需要使用lambda表达式……如果函数必须精确接受一个操作数,那么
|> foo(...)
语法有何用途?为什么必须这样写?为什么这不工作?
当这不工作时,为什么这也不工作?
显然,“foo(…)”只是PHP中函数引用的一种语法,根据文章中链接的“第一类可调用对象”RFC [1]。
所以在Python中,你会说例如
PHP 则需要使用以下语法:
至于该功能的整体目的,尽管它似乎可以被文章结尾提到的函数组合所替代,且函数组合可以通过实用函数而非专用语法实现,但添加这些运算符的优势显然是[2]性能(减少函数调用次数)和便于静态类型检查。
[1] https://wiki.php.net/rfc/first_class_callable_syntax
[2] https://wiki.php.net/rfc/function-composition#why_in_the_eng…
谢谢,这很有道理!
有一个相关的RFC关于部分函数应用,它将允许调用具有多个参数的函数。
https://wiki.php.net/rfc/partial_function_application_v2 https://wiki.php.net/rfc/pipe-operator-v3#rejected_features
这是为了在函数中将链式值插入到正确的位置。
他们提到Elixir有一个稍显高级的版本,大概就是这个意思,他们指的是(Elixir对参数个数大于1的函数有原生支持)
但示例表明它无法在正确位置插入链式值;如果真是这样,示例本应写为
|> array_column(‘tags’, ...)
。是的,这听起来很奇怪。默认使用第一个(或唯一)参数会更有意义。
虽然我欣赏这种努力并总体上喜欢这种方法,但在这个用例中,我更倾向于使用扩展函数(如 Kotlin[1]中的实现)或 IEnumerable/迭代器方法(如 C#中的实现)。
感觉比直接编写以下代码复杂得多
拥有针对列、扁平化、唯一值和值的数组扩展方法。
1: https://kotlinlang.org/docs/extensions.html#extension-functi…
Kotlin 还提供了扩展函数
let
(以及几个变体),允许你链式调用任意方法:` val arr = … val result = arr .let { column(it, “tags”) .let { merge(it) } .let { unique(it) } .let { values(it) } “`
对于单参数函数,你也可以添加函数引用:
“` arr.let(::unique) // 或 (List<>::unique),具体取决于函数 “`
所有这些操作均无需添加特殊语言构造。
基本上与 Laravel 中的 Collections 功能类似。
没错……在 Symfony 中也是如此。
虽然将数组转换为集合对象是一个可行的选择,但如果 Iterable / Traversable 有扩展方法,会感觉更“原生”。
PHP 有特性,只需发明该 API,将其放入特性中,然后添加到数据类中即可。
好吧,虽然 traits 可能是某些用例的解决方法,但带有标量数据类型的简单数组无法通过 traits 进行扩展。
虽然我知道 Symfony、Laravel 等中有 Collection 类,但我并不喜欢用类来包装 PHP 数组以获得方法链,即使使用生成器也是如此。
无法用特性来解决。此外,我认为特性应该非常谨慎地使用,而且在我看来,它们并没有那么多的用例不会造成代码臭味。
如果一个库提供了一个接受字符串并返回数组的函数,那该如何实现?我无法让它使用我的数组类。
你能给一个实际的例子吗?
正如文章中描述的,PHP管道无论如何都需要进行一堆封装,所以你可以直接这样做。有多种替代方案,从一个将原始数组转换为类的函数或方法,到涉及__invoke、__call、分发器等抽象的方案。
此外,期望不需要在库上添加 facades 有些可疑,根据我的经验,这非常常见。我认为你实际上不太可能想使用原始数组,而是希望在代码中利用类型检查。
如果我在一个应用中使用Laravel的Str,而我和第三方库都想添加一个可链式的方法,我该如何让API有意义?我们不能都继承Str。我看到的唯一选项是Macroable,而那是一个我宁愿避免的兔子洞。
Laravel开发者期望你使用Stringable::macro来扩展他们的“流式”API,如果你在使用Laravel工具链的组织中工作,这可能是对其他成员来说最不意外的选择。
我仍然不明白你想做什么,或者为什么第三方库期望在 Stringable 上有一个被硬编码的“流式”方法。
我个人不喜欢 Str/Stringable API:将文件路径、字符串、加密等方法混在一起让我觉得奇怪和困惑。实际上,我更倾向于使用 Symfony,原因就是这样。
管道的优势在于它不关心返回值的类型。
假设你在链条中间添加一个 reduce 方法。使用扩展方法时,这将是链条中最后调用的方法。而使用管道时,你只需将结果传递给下一个函数
是的,我同意。这是管道的优势——尽管在我看来,与链式方法相比,管道更难阅读和编写。
我认为文章中的用例仍然可以更轻松地通过扩展方法解决 🙂
是的,示例还应展示你可以使用任意函数,而不仅仅是库函数。例如,你自己的业务逻辑、验证等。
我喜欢管道操作符——这是我喜欢Elixir的一个地方,尽管许多语言都有它。它让推理变得容易得多:
VS array_values(array_unique(array_merge(…array_column($arr, ‘tags’))));
我看不出来这有什么难理解的,假设这是使用变量时的结果代码:
这样也更容易在每一步之后检查值。
正确命名事物是计算机编程中更具挑战性的任务之一。为那些最终会被丢弃的中间变量命名是浪费时间。此外,引入的变量越多,就越有可能在后续代码中意外重复使用变量名。
由于PHP允许在其中一个分支中初始化变量但在另一个分支中不允许,并且在传递未声明的变量时默认继续执行,因此声明更多变量可能会导致一类令人讨厌的错误,这些错误需要对核心语言进行重大(破坏性)更改才能完全消除。
我认为检查这一点比在管道链中添加 |> IO.inspect() 更容易
或者在管道链末尾添加 `|> dbg()`,让它在管道链的每一步都打印值
仅仅为了附加断点而修改代码在当今时代有点愚蠢。
作为一名每天使用PHP编程的人,这种方法的主要问题是它会污染作用域。这使得调试变得困难得多——你无法跟踪变量,且程序的当前状态变得复杂。依我之见,如果一个值是临时变量(如中间值),我们就不应能使用它。因此,方法链或嵌套函数调用可以防止这种情况。然后,当我们在这些函数之后断点时,我们看不到虚假值。它还防止未来有人修改这些一次性值。有人可以轻松地在中间插入逻辑或调用,修改某些内容并打破链式调用。
在PHP中,这种情况的一种预防方法是使用函数。但这样做只是为了作用域,这并不是函数的真正用途。这会引入其他烦人的问题。
更容易编写、复制粘贴、组合和注释
每行引入一个新变量与管道运算符相比会增加大量认知负荷。
使用管道操作符进行数据流处理要简单得多,而且更健壮(例如,使用变量进行重新排序会很麻烦,容易引入错误)。
这种变量流处理在生产代码中更难进行数据流处理,中间结果会成为难以过滤的噪声,而像 |> 这样的重复符号则更容易过滤。
建议确保函数与输入数据类型兼容,并尽量避免在链中中途破坏数据类型以导出数据。如果预期会出现此类错误,使用带->的构建器链可能是更好的选择,并在方法中进行日志记录。
你的版本包含4个变量。管道不会创建这些中间变量,因此更节省内存。
可读性主要取决于习惯。人们容易阅读自己习惯阅读的内容。
管道确实更易于阅读,对于许多情况而言它们是更好的选择,但嵌套函数的例子并不成立。
这就像说有人会使用以下代码:
这比嵌套函数更难理解。
或
这种写法更易于阅读且更优——你需要从最内层函数逐层解析到最外层才能理解其逻辑。而使用管道符时,逻辑更直观:你只需按步骤逐一阅读——先执行这个,再执行那个,接着执行下一个——就像自然阅读指令一样。
当需要为中间函数提供额外参数时,管道语法比嵌套函数调用更易于阅读。使用嵌套函数时,即使尝试通过格式化来辅助,也难以看清这些参数属于哪个函数。
为什么不也格式化管道呢?
vs
使用管道时,数据转换是线性序列。而使用嵌套函数调用时,你必须从最内层函数开始,一直向上追溯到最外层。
> 因此它们更节省内存
确实可能更节省内存。这取决于语言、解释器、编译器,以及是否对中间变量进行操作(优化器可将其移除)。
我以为我们在讨论PHP8.5:)
啊,我以为我们在更广泛地讨论允许避免中间变量的编程语言构造,抱歉 🙂
> 这样更容易理解
真的吗?我不这么认为。
在Elixir中效果会好一些,因为你很少需要在管道中包含一次性lambda。标准库函数是为管道操作符设计的,而你通常想传递的东西通常是第一个参数。
我喜欢这个。
我真的认为PHP最需要的是对字符串/数组函数进行重新设计,使其更加一致且可链式调用。现在它们至少可以链式调用了。
不过我并不喜欢 … 语法,尤其是在与展开运算符混合使用时
可以通过允许省略单参数函数中的 (…) 部分,并使用柯里化处理需要额外参数的函数来改进语法。这样你最终会得到类似以下形式:
同意,示例中每个 fn($x) 都使用 $x 作为其参数名称时,… 语法确实让人感到困惑。
我的第一反应是这样写:
`$result = $arr
这让我好奇这种写法如何处理作用域。我猜嵌套函数内部无法访问输入的 $arr,对吧?是否支持按引用传递?
你可以这样写。参数名称是任意的。而且,据我所知,你无法访问前一个作用域中的变量
你可以这样做
来捕获本地环境中的内容。
编辑:而且你可以按引用传递:
虽然在实际中从未这样做过,但不确定除了远程修改的明显风险外,是否还有其他潜在问题。
PHP的字符串/数组函数是一致的。
字符串函数使用(haystack, needle),而数组函数使用(needle, haystack)
因为底层的 C 库也是这样工作的
但事实并非如此。
array_filter 接受 (arr, callback)
https://www.php.net/manual/en/function.array-filter.php
array_map 接受 (回调函数, arr)
https://www.php.net/manual/en/function.array-map.php
array_map 是可变参数的。它实际上是 (回调函数, …arr)
一个函数作用于单个元素,而另一个作用于多个元素。在这种情况下,参数顺序更有意义。如果你想使用 (arr, callback),可以使用 array_walk,但它只作用于单个数组——与 array_filter 类似。
这是“英语句子顺序一致”的。
数组过滤是“用这个函数过滤这个数组”。
数组映射是“将这个函数映射到这个数组上”。
但我同意任何替换函数都应与Haskell保持一致。
可以以相反的顺序构建英语句子。不存在唯一的“英语句子顺序”。
“用这个函数过滤这个数组”
“使用这个函数对这个数组进行映射”
但这不正确。array_map 是可变参数的。因此它实际上应该是“使用这个函数对这些数组进行映射”。
当你使用正确的措辞时,参数顺序是有意义的。
没错,但这两种说法都更不自然。
在现实世界中,人们用某种东西过滤另一种东西,比如用网筛过滤水等。
而在数学中(至少在数学中),人们将某种东西映射到另一种东西上。(较不常见的是将某个区域映射到纸上等。)
仅仅因为你能构造出这两种句子,并不意味着它们是自然的词序。
> 而在数学中,至少,一个将某物映射到另一物。
是的,但这与你之前说的相反。例如,你可以将x映射到2*x。或者,如果你在谈论一个集合,你可以将整数0..10映射到其值的两倍。数据优先,然后是操作方式。我是数学家,这对我来说有意义。
我只会说“映射这个函数…”如果函数本身正在以某种方式被操作(映射到其他值)。
考虑到PHP被数十万非英语母语者使用,我认为你无法合理地声称“英语句子顺序”优于“参数顺序的一致性”。
网上有足够多的病毒视频显示,即使是相邻的欧洲国家,常见句子的顺序也不同。就连读时间(上一小时的半点 vs 下一小时的半点)和计数方式在不同语言中也不同。
因此,基于英语口语习惯来设计参数顺序,对于使用于全球各地程序员的编程语言而言,并不合理。
> 考虑到PHP被数十万非英语母语者使用,我认为你无法合理地声称“英语句子顺序”优于“参数顺序的一致性”。
这很好,因为我没有这么做。
> 因为底层的C库也是这样工作的
我认为这是对内部不一致行为的弱辩护。作为一名使用PHP编程超过二十年的人,其中大部分是专业工作,我仍然无法记住这些函数中的针/草堆顺序,我感谢智能感知功能让我保持理智。
正如这个管道操作符所示,或者以属性为例,PHP无需严格遵循C语言的实现方式,那么为何不改进它,而是将其归因于“它就是这样,因为它一直都是这样”?
这与其说是辩护,不如说是对历史起源的解释。就连语言的创建者也不为这些不一致性辩护,并承认它们是错误。PHP 非常重视向后兼容性,不会仅仅为了保持一致性而重新排列事物。
所以它们一致是因为它们一直不一致??
PHP没有理由继承C语言在这里的问题。
在PHP早期,它高度依赖于封装底层C库并保留其命名约定。
https://news-web.php.net/php.internals/70950
在早期阶段,这确实没问题,绝对没问题。
但我们已经不再处于早期阶段,而且在许多其他方面,PHP 已经有了很大的发展。
有什么好处?代码补全、AI 代理等功能会为你处理这些问题。没有人会因为参数顺序更接近 C 语言而非十年前某篇博客文章的抱怨而陷入困境。PHP 开发者已经有 30 年的时间来学习这些差异。C 语言开发者会抱怨这个问题吗?
如果我们想改变PHP中字符串/数组函数的参数顺序,我认为应该从修复C库开始。这似乎是一个更好的起点。影响将对更多开发者更有利,而不仅仅是PHP开发者。
因为这样会更可预测、更易于记忆、更简洁、对来自其他现代语言的开发者更易于使用,并且更舒适地工作。
它们自30年前以来一直混乱,这并不是现在继续保持混乱的合理理由。
此外,我甚至不主张修改现有函数,因为这几乎毫无理由地会破坏所有现有代码。
我认为他们应该“简单地”支持原始类型的方法,并以可链式的方式实现主要方法:
“test string”->trim()->upper()->limit(100);
[0,1,2]->filter(fn ($n) => $n % 2 === 0)->map(fn($n) => $n * 2);
我非常喜欢这个想法
`strlen`、`strncmp` 和 `strtolower`,但不包括 `str_split` 和 `str_contains`。
这是否一致?
PHP 是一个奇怪的生物,没有人愿意称赞它,但它对那些能够驯服它的人来说却非常有效。
我可能永远不会触碰它,因为有太多语言可以使用,而我所知道的已经足够完成我的工作,但我对像 PHP 这样在我圈子外的主流语言继续发展感到非常兴奋
我不是在诱惑你去尝试,但我想说,从你的角度来看,如果有一天你需要一个粗糙的应用程序并尝试使用 Laravel 实现,你可能会对现代 PHP 的实际能力感到惊讶。
曾经有一段时间,我认为这门语言及其生态系统正在走向衰落,但后来它们恢复了,现代PHP现在有90%的功能都能满足你的需求,你不需要担心实现方式,它很简单。
我不再经常使用它,但每次使用时,我看到的都是无限的可能性。
部署方面呢?我假设需要像Python那样使用SCP传输文件,或者将所有内容放在一个巨大的PHP文件中?这是个选项吗?
如今的部署基本上就是git pull && composer update
当然,如果你使用虚拟机或无服务器架构等,情况不同,但对于一个基本的粗糙应用程序,这就是你要做的。
或者如果你想走老派路线,当然可以,只需 scp 那个目录,它仍然像 30 年前一样有效。
太棒了,谢谢
Laravel Forge 在推送到主分支时自动处理部署。或者如果你想要生产环境零停机部署,使用 Laravel Envoyer。
我乐意称赞它。这是一种非常实用的语言,拥有良好的工具链和海量的库及脚本。性能尚可,但工具、玩具和原型开发的效率极高。
标准库提供了大量用于调用API、处理JSON、执行shell命令、字符串操作和通过套接字发布HTML的功能。在典型安装中,还包含常见的数据库接口。多年来,我一直在单文件PHP脚本和PsySH中以令人惊叹的速度解决大量问题。
多线程支持并不理想,因此通常我会在PHP中实现逻辑,然后通过Scheme、Picolisp或Elixir等语言进行驱动。
标准库如此不一致,这将是一场噩梦。
使用更好的语言时,你知道参数传递的顺序(array_map/array_filter),但在PHP中这完全是碰运气。
这感觉像是硬塞进去的,完全不适合标准库。
PHP开发者应首先专注于实现完整的Unicode支持(不,mb_real_uppercase无法满足要求),然后再专注于设计一个新的命名空间标准库。
>标准库如此不一致,这将是一场噩梦。
我认为在这种情况下,可调用对象将变得毫无用处,每个人都会将闭包管道传输到标准库强制要求的位置。
这就是问题所在。
我们确实需要一个更好的标准库,配备适当的数据结构
这是一个先有鸡还是先有蛋的问题
我认为此类倡议将推动对更一致标准库的需求,即使速度较慢,PHP 也在逐步废弃/重构其标准库,因此对此抱有希望。
数组_map 和数组_filter 是否是常见的参数?前者针对单个元素,而后者针对多个元素。您建议更好的参数顺序是什么?您知道数组_walk 存在吗?
Raku 自诞生以来就拥有类似的 feed 操作符
使用 ==> 和 <== 表示向左
虽然这是语法糖,但管道输入往往非常有用,能让链式调用一目了然
https://docs.raku.org/language/operators#infix_==%3E
这让我想起了 D 语言的统一函数调用语法[0], 它允许你将 bar(foo(sort(myArray))) 重写为 myArray.sort().foo().bar()。区别在于 D 语言允许额外的函数参数,并将传入的值作为第一个参数。例如,你可以使用 myArray.sort().writeln(“额外文本”)。
[0]: https://tour.dlang.org/tour/en/gems/uniform-function-call-sy…
每个步骤都会缓冲到一个临时变量中——这不像 Bash 管道那样高效。
来自非 PHP 用户的真诚问题:
PHP 是否支持类似迭代器的对象?就像 Python 一样,其中 mydict.values() 会按需返回值,而不是立即实现为列表。还是说所有步骤都必须保证完全实现为一个完整的列表?
是的,很久以前就支持了——https://www.php.net/manual/en/class.iterator.php
有趣,但我特别想知道文章中示例中的转换是否确实采用了这种协议。那些转换是否利用了这个协议?我最初回复的评论似乎暗示它们没有。
文章中提到函数组合的部分暗示了这一点。文章指出,在将函数传递给map之前进行组合是一种优化。我理解这意味着,如果没有组合,每个map都会完全处理从前一个map传递过来的数组,而示例中的第一个map会完全读取整个文件。如果它是可迭代的,那么函数组合与多个map组成的管道相比没有区别。
同时,我对为什么有时使用“map”,有时使用“array_map”感到困惑。后者是我熟悉的,我知道它对整个数组进行操作且不进行懒惰评估。如果“map”不仅仅是简写,而是实际创建了一个懒惰评估的可迭代对象,那么我对函数组合为何会产生任何影响感到困惑。
PHP确实支持生成器和迭代器,尽管我个人很少直接使用它们。
这是所有语言中函数调用的工作原理。除非是流。
我曾尝试用PHP模拟类似的功能。但 PHP 的问题在于参数顺序。尤其是在像 array_key_exists() 这样的函数中,数组元素是第二个参数,而管道运算符期望要操作的对象是第一个参数,即这些情况下的数组。
我相信他们现在已经解决了这个问题。不过不清楚具体如何实现。
通常的解决方案是使用闭包包裹。
或使用箭头函数语法:
同样的技巧也适用于需要使用强制额外参数的函数、按值传递参数的函数等。
如果部分函数应用 RFC 通过,则闭包将不再必要
https://wiki.php.net/rfc/partial_function_application_v2
这看起来很不错。然而,自从我读到Koka的点选择[0]后,我一直觉得这种语法更简洁:
fun showit( s : string )
然而,当然,在大多数语言中,这无法实现,因为点符号已经用于其他用途。
[0] https://koka-lang.github.io/koka/doc/book.html#sec-dot
这被称为统一函数调用语法。
D语言早已支持这一特性:https://tour.dlang.org/tour/en/gems/uniform-function-call-sy…
Nim 也有此特性:https://nim-by-example.github.io/oop/
该语法在正常工作时非常简洁。然而,我认为其局限性在于无法将参数管道传输到第2、第3等位置,也无法支持关键字参数或类似文章中展示的可变参数展开,这使得其功能较为有限。
该语言中是否存在其他语法辅助工具来克服这一限制?
对于简单情况,保持简洁的语法仍有意义。你可以使用柯里化(无论是否支持一等公民语言特性)来处理更复杂的情况,或者退而求其次使用传统的函数组合,甚至循环。
我认为这被称为统一函数调用语法。
嗯。看起来PHP确实新增了一个现代特性,而且效果不错,不像往常的新PHP特性那样,在其他语言中已是标准的特性在这里却显得更差。我感到惊讶的是,他们似乎在这方面做得不错。而且他们还巧妙地避免了创建新类型表达式却无法覆盖所有情况的陷阱。
我猜这可能不太为人所知(这我完全理解):Vimscript有一个类似管道效果的箭头运算符,比如foo->bar(baz)->qux()。参见文档:https://vimhelp.org/eval.txt.html#method。
PHP 获得一个管道操作符并实现 Maybe Monad 的方式,绝对不在我的 2025 年愿望清单上。
但任何使主流语言更具函数式特性的改动都非常值得欢迎!它只是比命令式代码更符合人体工学。
使用 Clojure 的众多乐趣之一 https://clojure.org/guides/threading_macros
这很酷。不过我个人可能不会使用它。我认为使用几个临时变量或专用函数来处理需要超过2或3次迭代操作的计算,对可读性和可维护性更好。
这篇文章很好地阐述了管道运算符的用途,但为什么不直接重写这些函数以支持方法链呢?` $profit = [1, 4, 5] .loadSeveral() .filter(isOnSale()) .map(sellWidget()) .array_sum(); ` 这还有一个额外的好处,就是“看起来正常”
向后兼容性。过去10年,该语言在添加新功能的同时,几乎没有破坏大量旧代码,这做得相当出色。我认为PHP仍运行着约75%的互联网,这相当庞大。
Python 2到3的升级就是向后兼容性多么重要的例子
因为管道适用于所有函数,而不仅仅是对象方法。因此你的业务逻辑、验证等无需成为内置对象的方法。
管道本身并没有什么异常之处
这是一种有趣的编程范式,类似于 Java 的流与 lambda 表达式。对可读性很有帮助。不过我对 |> 运算符不太感冒,因为在我的键盘布局下需要按下 4 个不同的按键,不太符合人体工学。但我也理解当时选择有限,而且它也算得上清晰。
只有我一个人觉得它丑陋吗?
每种语言都应该拥有这个功能。
无需改造现有代码,它让新代码变得更加合理(当函数变得简单时,产生面向对象垃圾的冲动会大大减弱)——它们被称为编程语言是有原因的。
我专业使用PHP十年,至今仍不明白为何在2025年我们需要重新发明几乎在每种语言中都已成标准的语法
Rust是下一个?开玩笑归开玩笑,编程语言中的管道运算符有一个有趣的副作用,即支持面向管道的编程,这是我在不使用F#时最怀念的功能。
在 Rust 中,函数/特性的解析方式实际上已经非常符合这种编码风格(只需使用点运算符)。标准库 Iterator 就是一个很好的例子。:-)
我认为目前还没有推动更简洁语法的明显动力。
有一个名为 `tap` 的 crate (https://crates.io/crates/tap),它将 `tap`、`pipe` 及其变体添加到所有内容中。
“管道运算符的一个主要限制是,链中的所有可调用对象都只能接受一个必需的参数。
对于内置函数,如果函数不接受任何参数,则无法在链中使用。对于用户空间的 PHP 函数,向不接受任何参数的函数传递参数不会引发错误,而是会被静默忽略。
使用 pipe 运算符时,前一个表达式或可调用函数的返回值始终作为第一个参数传递给下一个可调用函数。无法更改参数的位置。"
https://php.watch/versions/8.5/pipe-operator
鉴于这些限制,我不会称 Elixir 的实现为“稍微花哨一些”。
我不确定是否会为了这个功能升级本地 PHP 版本,但他们添加这个功能还是很不错的,我相信有很多库代码如果重写成这种风格会看起来好得多。
希望他们能重新考虑在 Ruby 中实现这个功能
我对以下代码的逻辑感到困惑:
为什么需要这个内联函数?为什么不直接使用
?
我的意思是,我明白这是因为这个运算符的设计方式。但为什么?
> |> array_column(…, ‘tags’)
这种语法是无效的。但明年随着提议的局部函数应用RFC,它将变得可能
array_column(?, ‘tags’)
https://wiki.php.net/rfc/partial_function_application_v2
为什么不直接让类型成为伪对象?
$myString.trim().replace(“w”, “h”);
这样做的好处是,它还提供了一个干净的替代方案,以取代碎片化的标准库。
> 为什么不直接将类型定义为伪对象呢?
用这种“直接”的方法,我可以用火柴棍建起巴黎
我同意。但在PHP中,这可能看起来像这样:
$myString->trim()->replace(“w”, “h”);
因为管道不关心函数返回的类型。你不需要在每个类型上添加数百个方法以防万一。你只需将前一个函数的结果传递给下一个函数。
这些函数可以是业务逻辑、验证逻辑,或是其他内容。而不仅仅是对象方法
因为复制标准库可能不是个好主意。
感谢 F#!
PHP:
Ruby:
我明白这不是管道操作符,但看看这两种语言之间的字符差异。
// <- 这是怎么回事
这是我的个人看法。
Ruby 凭借其灵活性,难道不能轻松模拟管道操作符吗?
我想到的是这样的实现:
然后可以这样使用:
或者如果我们为 then 方法创建一个别名:
那么它可以像这样使用:
这个管道运算符的优点在于它可以接受任何可调用表达式。我正在编写一个库,以使这些数组和字符串函数更加表达力。
例如,在 PHP 8.5 中,你可以这样做:
[1,1,2,3,2] |> unique
然后将“unique”定义为一个常量,并为其分配一个回调函数,大致如下:
const unique = static fn(array $array) : array => array_unique($array);
这样更好。
在新的行尾添加分号有助于避免 Git 冲突并生成更干净的差异。
这是 PHP 允许在所有列表中使用尾随逗号的相同原因。
将分隔符放在单独一行是一种语法技巧,有助于将小改动压缩为更整洁的单行差异,而非两行差异。你可能在其他上下文中多次遇到过这种情况而未曾留意。例如数组/哈希、带引号的多行字符串等。
非常棒,F# 的优秀特性,希望未来能在更多语言中看到!
看到 PHP 不断发展令人欣慰。它已远非我当年使用 V3 版本时的模样。我曾以为其糟糕的设计会让它走向末路,但核心开发者们坚持不懈,如今它确实已成为一门相当不错的语言。
轮到你了,JavaScript。
组合功能会比这更好,也许很快就会实现
可能比你想象的要快。已经有关于部分函数应用和函数组合的RFC:
https://wiki.php.net/rfc/partial_function_application_v2 https://wiki.php.net/rfc/function-composition
快点吧,Dart!请跟进。Go已经没救了…
我感觉自己像个幼儿园孩子在写Go。我希望另一种语言能在Go被使用的领域流行起来。
Kotlin 正在逐步发展。它已经具备了原生编译器,且随着每次发布不断改进,同时拥有不错的跨平台库。但在原生库支持和 POSIX 相关功能方面稍显薄弱。不过这是一个可解决的问题,只需更多人投入相关开发即可。
例如,ktor(其中一个服务器框架)实际上可以与 Kotlin 原生版本配合使用,但支持程度并不高。这并不使用 Graal 或任何 JVM 运行时组件(当然这也是一个可行的路径,但会更加复杂)。使用 Kotlin 原生版本时,Kotlin 编译器会直接编译为原生代码,并使用具有原生实现的多平台库。不使用 Java 标准库,也不使用任何 JVM 库。
同一编译器也为 iOS 原生环境提供支持,通过 Compose 多平台实现。在 iOS 平台上,库的覆盖范围更为全面,正逐渐成为 Flutter 和 React Native 等技术的可行替代方案。它还具备相当不错的 Objective-C 和 Swift 集成能力(双向兼容),目前正致力于进一步优化。
无论如何,用 Kotlin 编写命令行工具相当简单。可使用 Klikt 或类似工具进行命令行参数解析。
JetBrains似乎在某些方面忽视了这一点。在我看来,这是一个盲点。他们的WebAssembly支持也存在类似问题。在浏览器中运行效果良好(并支持Compose),但目前还不是无服务器计算或边缘计算的明显选择;主要是因为库支持不足。
Swift 更显而易见,但问题在于苹果似乎将其视为推广其操作系统供应商锁定的库,而非通用编程语言。两者在系统编程任务中都有潜力与 Go 竞争。
这很有趣,我得留意一下。
我一直认为 Kotlin 只适用于 Android 和 JVM,所以从未关注过它
这太棒了!向 PHP 致敬。我第一次在 Elixir 中遇到管道,此后在其他语言中都感到缺失。两点观察:
– 管道让你意识到,为了实现一些非常简单的事情,你需要做多少额外的工作。嵌套、中间变量等都掩盖了实际上非常有序的一系列操作。
– 管道确实必须是语言的一级操作符。我曾在没有管道的语言中尝试使用类似管道的语法糖,虽然它能完成任务,但失去了很多优雅和简洁性。感觉就像在使用一个绕弯子的东西,因此最终无法达到相同的简洁程度。如果以非设计用途的方式使用语言,事情可能会变得非常混乱。尽管我喜欢管道,但我见过在没有管道的语言中,“假管道”反而让事情变得更复杂。
说实话,我希望 Python 也有类似的功能。
可以梦想,但不要抱太大希望。我感觉函数式编程模式在Python中被视为二等公民。
如果JS或TypeScript也能加入这个行列就好了!
那么PHP现在可以像Clojure一样编程了吗?
今年将是PHP的时代。人们已经厌倦了JS。
我确实对JS感到厌倦;然而,我也不喜欢PHP。我喜欢新管道语法作为一个概念,但当它被添加到一个已经不舒服的整体编程环境中时,它只能提供轻微的缓解。
我宁愿写JS/TS,也不愿写其他大多数流行语言。
你是说PHP的第四个十年。
我钦佩你的坚定信念。
我并不喜欢它,但JavaScript会比我活得更久。
[删除]
什么?
提示:提及这将如何影响每个主题的人工智能。
我只是在开玩笑。如今人们对PHP的欣赏程度远不及其他语言。
语法丑陋至极。
感谢你的见解。
阿门……我的意思是,如果语法不是这么令人却步,PHP本可以成为一门很好的语言。
哇,死语言还加了个特殊字母 wowooeoowowoowoo
它死得这么彻底,以至于79.2%的网站都依赖PHP到某种程度。
https://kinsta.com/php-market-share/
PHP从什么时候开始被称为死语言了?
为什么PHP不取消变量使用的$符号和调用方法使用的->符号?我认为仅此两点就能大大提升其形象和采用率,比添加管道运算符更有效果。
我其实并不介意这些符号,而且我已经有好几年没有每天使用 PHP 了。当我看到有人在 JS 中用 _ 表示内部变量或用 $ 表示元素时,我感到很不舒服,但在 PHP 中,$ 符号反而显得挺不错的。
我更喜欢 -> 的外观,它很 _酷_
其他语言都有各种各样的超大箭头,比如 ==> 和 >>>。
相比之下,PHP 和 C++ 中的 -> 看起来很简洁。
不过,我永远不会原谅他们对命名空间分隔符的拙劣设计。
命名空间分隔符的替代方案是什么?反斜杠与 PSR-4 规范配合良好,能直观地展示预期目录结构。
由于 PHP 借鉴了大量 C 类语言的语法,C++ 的命名空间分隔符 :: 显然是最佳选择。不过不确定这是否与 PHP 中的其他内容冲突。
https://www.php.net/manual/en/language.oop5.paamayim-nekudot… 作用域解析运算符
> 我永远不会原谅他们对命名空间分隔符的拙劣处理。
你是指反斜杠吗?那有什么问题吗?
对于不熟悉PHP的人来说,这看起来像是你在尝试转义某个内容。
反斜杠在所有编程语言中都被保留为转义字符。
这个决定几乎20年前就已做出,所以我已经完全习惯了,再争论也没意义。但将反斜杠重新用作命名空间分隔符的决定仍会不时带来不便。例如,在composer.json中编写PSR-4配置时,所有反斜杠都需要成对出现,包括(尤其是!)末尾的反斜杠。
与C语言不引入类、C++不移除指针的原因相同:a) 这是核心语言的一部分,b) 对任何严肃的开发者而言都微不足道。
我实际上喜欢这些美元符号为代码库带来的清晰度。这使得识别(动态)函数更容易,同时也减少了意外覆盖方法的可能性。
其他语言允许你做 `const Math = {}` 并彻底清除整个数学库,或编写类似 `int fopen = 0;` 的代码以在该作用域内使 fopen 方法调用不可用。在 PHP 中,你无需将变量名限制为“希望不会与某个晦涩方法冲突的名称”。
-> 是来自较旧编程语言的遗留符号,我更希望用 . 替换它,但不能以破坏现有代码为代价(这肯定会发生)。
> -> 是来自较旧编程语言的遗留符号,我更希望用 . 替换它,但不能以破坏现有代码为代价(这肯定会发生)。
难道不是因为 PHP 中 . 已经用于字符串连接吗?我的意思是,-> 语法并非 PHP 发明,但它也没有毫无思考地继承它。
确实。PHP 使用 . 进行字符串连接是因为 Perl 这样做。
我也欣赏PHP拥有显式的字符串连接运算符而非重载+。当然,如果我们谈论的是时间旅行,它本可以使用另一个符号来消除->。但目前,你无法真正使用$obj.method(),因为method()也可能是一个返回字符串的函数,因此存在歧义。
因为向后兼容性是语言的一个非常强大的特性,这也是为什么创建了“match”而不是替换“switch”的原因。
因此,将一个 PHP 5.2 脚本迁移到 8.5 非常容易,而将一个 PHP 4 脚本迁移则稍难一些,只是耗时更长(因为它可能使用了像 register_globals 这样的恐怖特性)。
最终,我更倾向于这种方式,而非一个无法解决的碎片化生态系统。
PHP确实存在显著缺陷,但表面上的语法差异并不在其中。根据我的经验,适应任何语法大约需要两周时间。
我真的不明白这一点。语法是编程语言中最无聊的部分之一。这个问题可以通过 IDE(现在还有大语言模型(LLMs))来解决。我不在乎语法,我在乎的是我能构建什么。自古以来,人们就争论着制表符与空格、美元符号等事情,老实说,我不明白为什么。这根本不重要。
需要明确的是:一致性非常重要。阅读完全不同的代码风格会带来巨大的心理负担,而且浪费精力。
可读性(以及由此带来的可维护性)确实是此类决策的重要因素。但在管道运算符的具体案例中,文章确实提到了它能实现之前无法实现的功能:在仅允许表达式的上下文中(如匹配操作),现在可以执行之前因需要临时变量而无法实现的操作。
$符号让我在刚开始编程时得了腕管综合征。我永远不会原谅PHP这一点。这种语法实在不够人性化。
> 为什么 PHP 不去掉变量前那个讨厌的 $ 符号和调用方法时用的 -> 符号?我认为光是去掉这两个符号,对提升 PHP 的形象和普及度就比添加管道运算符更有帮助。
因为它无法以向后兼容的方式实现这一点。-> 符号并不算太糟糕,C/C++ 语言也使用了它。至于 $ 符号,我猜它源自 Perl 语言。这个符号已经用于字符串连接,而其他语言会重载 + 运算符来实现相同功能。
“本质上与 shell 管道相同”,但每个函数会依次完整执行,并将输出保存在变量中。因此与 shell 管道完全不同。
对于简短的构造,‘$out = sort(fn($in))’ 确实更易于阅读。对于较长的构造,可以将其拆分为多行。
显式使用临时变量真的会增加“认知开销”吗?显式化有时是一种美德。可读性在编程语言中至关重要。如果没什么别的,我认为 Python 已经教会了我们这一点。
我对这类语法糖持怀疑态度。通常你真正需要的是一个迭代器。隐藏这种需求的能力显然存在风险。