PHP 8.5 adds pipe operator

2025-08-054:13435274thephp.foundation

The PHP Foundation — Supporting, Advancing, and Developing the PHP Language

PHP 8.5, due out November of this year, will bring with it another long-sought-after feature: the pipe operator (|>). It's a small feature with huge potential, yet it still took years to happen.

What is a pipe operator?

The pipe operator, spelled |>, is deceptively simple. It takes the value on its left side and passes it as the single argument to a function (or in PHP's case, callable) on its right side:

$result = "Hello World" |> strlen(...)

// Is equivalent to
$result = strlen("Hello World");

On its own, that is not all that interesting. Where it becomes interesting is when it is repeated, or chained, to form a "pipeline." For example, here's real code from a real project I've worked on, recast to use pipes:

$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']

The same code without pipes would require either this horribly ugly nest of evil:

array_values(array_unique(array_merge(...array_column($arr, 'tags'))));

Or manually creating a temporary variable for each step. While temp variables are not the worst thing in the world, they are extra mental overhead, and mean that a chain like that cannot be used in a single-expression context, like a match() block. A pipe chain can.

Anyone who has worked on the Unix/Linux command line will likely recognize the similarity to the shell pipe, |. That's very deliberate, as it is effectively the same thing: Use the output from the left side as the input on the right side.

Where did it come from?

The |> operator appears in many languages, mostly in the functional world. F# has essentially the exact same operator, as does OCaml. Elixir has a slightly fancier version (which we considered but ultimately decided against for now). Numerous PHP libraries exist in the wild that offer similar capability with many extra expensive steps, including my own Crell/fp.

The story for PHP pipes, though, begins with Hack/HHVM, Facebook's PHP fork née competitive implementation. Hack included many features beyond what PHP 5 of the day offered; many of them eventually ended up in later PHP versions. One of its features was a unique spin on a pipe operator.

In 2016, Sara Golemon, long-time PHP contributor and former Open Source lead on the HHVM project, proposed porting Hack's pipes to PHP directly. In that RFC, the right side of a pipe wasn't a callable but an expression, and used a magic $$ token (lovingly called T_BLING, at least according to yours truly) to inject the left-side result into it. In that case, the example above would look like this:

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

While powerful, it was also somewhat limiting. It was very non-standard, unlike any other language. It also meant a weird, one-off syntax for partially-calling functions that worked only when paired with pipes.

That RFC didn't go as far as a vote. Nothing much happened for several years, until 2020/2021. That's when I, fresh off of writing a book on functional programming in PHP that talked about function composition, decided to take a swing at it. In particular, I partnered with a team to work on Partial Function Application (PFA) as a separate RFC from a more traditional pipe. The idea was that turning a multi-parameter function (like array_column() above) into the single-parameter function that |> needed was a useful feature on its own, and should be usable elsewhere. The syntax was a bit different than the Hack version, in order to make it more flexible: some_function(?, 5, ?, 3, ...), which would take a 5-or-more parameter function and turn it into a 3 parameter function.

Sadly, PFA didn't pass due to some engine complexity issues, and that largely undermined the v2 Pipe RFC, too. However, we did get a consolation prize out of it: First Class Callables (the array_values(...) syntax), courtesy Nikita Popov, were by design a "junior", degenerate version of partial function application.

Fast-forward to 2025, and I was sufficiently bored to take another swing at pipes. This time with a better implementation with lots of hand-holding from Ilija Tovilo and Arnaud Le Blanc, both part of the PHP Foundation dev team, I was able to get it through.

Third time's the charm.

More than the sum of its parts

Above, we described pipes as "deceptively simple." The implementation itself is almost trivial; it's just syntax sugar for the temp variable version, effectively. However, the best features are the ones that can combine with others or be used in novel ways to punch above their weight.

We saw above how a long array manipulation process could now be condensed into a single chained expression. Now imagine using that in places where only a single expression is allowed, such as a 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.
};

Or, consider that the right-side can also be a function call that returns a Closure. That means with a few functions that return functions:

$profit = [1, 4, 5] 
    |> loadSeveral(...)
    |> filter(isOnSale(...))
    |> map(sellWidget(...))
    |> array_sum(...);

Which... gives us mostly the same thing as the long-discussed scalar methods! Only pipes are more flexible as you can use any function on the right-side, not just those that have been blessed by the language designers as methods.

At this point, pipe comes very close to being "extension functions", a feature of Kotlin and C# that allows writing functions that look like methods on an object, but are actually just stand-alone functions. It's spelled a bit differently (| instead of -), but it's 75% of the way there, for free.

Or take it a step further. What if some steps in the pipe may return null? We can, with a single function, "lift" the elements of our chain to handle null values in the same fashion as null-safe methods.

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(...));

That's right, we just implemented a Maybe Monad with a pipe and a single-line function.

Now, think about that for streams...

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(...))
;

The potential is absolutely huge. I don't think it's immodest to say that the pipe operator has one of the highest "bangs for the buck" of any feature in recent memory, alongside such niceties as constructor property promotion. And all thanks to a little syntax sugar.

What comes next?

Although pipes are a major milestone, we're not done. There is active work on not one but two follow-up RFCs.

The first is a second attempt at Partial Function Application. This is a larger feature, but with first-class callables already bringing in much of the necessary plumbing, which simplifies the implementation. With pipes now providing a natural use case, as well as easy optimization points, it's worth a second attempt. Whether it makes it into PHP 8.5, is delayed to 8.6, or is again rejected is still an open question as of this writing, though I am hopeful. Major thanks to Arnaud Le Blanc from the PHP Foundation team for picking it up to update the implementation.

The second is a function composition operator. Where pipe executes immediately, function composition creates a new function by sticking two functions end-to-end. That would mean the streams example above could be further optimized by combining the map() calls:

fopen('pipes.md', 'rb')
    |> decode_rot13(...)
    |> lines_from_charstream(...)
    |> map(str_getcsv(...) + Product::create(...) + $repo->save(...))
;

This one is definitely not going to make it into PHP 8.5, but I am hopeful that we'll be able to get it into 8.6. Stay tuned.

Special thanks to Ilija Tovilo and Arnaud Le Blanc from the PHP Foundation team for their assistance with the pipe implementation. If you’d like to help push PHP forward, consider becoming a sponsor.


Read the original article

Comments

  • By sumeetdas 2025-08-0511:035 reply

    The first typed programming language where I've seen pipe operator |> in action was in F#. You can write something like:

      sum 1 2
      |> multiply 3
    
    
    and it works because |> pushes the output of the left expression as the last parameter into the right-hand function. multiply has to be defined as:

      let multiply b c = b \* c
    
    
    so that b becomes 3, and c receives the result of sum 1 2.

    RHS can also be a lambda too:

      sum 1 2 |> (fun x -> multiply 3 x)
    
    
    |> is not a syntactic sugar but is actually defined in the standard library as:

      let (|>) x f = f x
    
    
    For function composition, F# provides >> (forward composition) and << (backward composition), defined respectively as:

      let (>>) f g x = g (f x)
      let (<<) f g x = f (g x)
    
    
    We can use them to build reusable composed functions:

      let add1 x = x + 1
      let multiply2 x = x \* 2
      let composed = add1 >> multiply2 
    
    
    F# is a beautiful language. Sad that M$ stopped investing into this language long back and there's not much interest in (typed) functional programming languages in general.

    • By christophilus 2025-08-0511:261 reply

      F# is excellent. It’s tooling, ecosystem, and compile times are the reason I don’t use it. I learned it alongside OCaml, and OCaml’s compilation speed spoiled me.

      It is indeed a shame that F# never became a first class citizen.

      • By cosmos64 2025-08-0511:41

        Lots of this, especially the tooling and ecosystem, improved considerably in the last couple of years.

        OCaml is a great language, as are others in the ML family. Isabelle is the first language that has introduced the |> pipe character, I think.

    • By Akronymus 2025-08-0517:39

      There are also ||> and |||> for automatically destructuring tuples and passing each part as a separate value along.

      And there are also the reverse pipes (<|, <|| and <|||)

      F# is, for me, the single most ergonomic language to work in. But yeah, M$ isn't investing in it, so there are very few oppurtunities to actually work with f# in the industry either.

    • By tracker1 2025-08-0516:12

      It's kind of wild that PHP gets a pipe(line) operator before JS finalizes its' version... of course they've had multiple competing proposals, of which I liked the F# inspired one the most... I've stopped relying on the likes of Babel (too much bloat) or I'd like to be able to use it. I used it for years for async functions and module syntax before they were implemented in node and browsers. Now it's hard to justify.

    • By rpeden 2025-08-0514:452 reply

      Is |> actually an operator in F#? I think it's just a regular function in the standard library but maybe I'm remembering incorrectly.

      • By laurentlb 2025-08-0515:27

        It's defined in the standard library and can be redefined by anyone.

        It's usually called operator because it uses an infix notation.

      • By int_19h 2025-08-0516:46

        All operators are functions in F#, e.g. this is valid: (+) 1 2

    • By dmead 2025-08-0512:431 reply

      Haskell seems pretty dead as well. Good think php has another option for line noise though.

      • By gylterud 2025-08-0513:553 reply

        What makes you believe Haskell is dead or even dying? New versions of GHC are coming out, and in my experience, developing Haskell has never been smoother (that’s not to say it is completely smooth).

        • By munificent 2025-08-0517:051 reply

          Compare the Redmonk rankings in 2020 to 2025:

          https://redmonk.com/sogrady/2020/02/28/language-rankings-1-2...

          https://redmonk.com/sogrady/2025/06/18/language-rankings-1-2...

          I think of languages as falling in roughly 3 popularity buckets:

          1. A dominant conservative choice. These are ones you never have to justify to your CTO, the "no one ever got fired for buying IBM" languages. That's Java, Python, etc.

          2. A well-known but deliberate choice. These are the languages where there is enough ecosystem and knowledge to be able to justify choosing them, but where doing so still feels like a deliberate engineering choice with some trade-offs and risk. Or languages where they are a dominant choice in one domain but less so in others. Ruby, Scala, Swift, Kotlin.

          3. Everything else. These are the ones you'd have to fight to use professionally. They are either new and innovative or old and dying.

          In 2020, Haskell was close to Kotlin, Rust, and Dart. They were in the 3rd bucket but their vector pointed towards the second. In 2025, Kotlin and Dart have pulled ahead into the second bucket, but Haskell is moving in the other direction. It's behind Perl, and Perl itself is not exactly doing great.

          None of this is to say that Haskell is a bad language. There are many wonderful languages that aren't widely used. Popularity is hard and hinges on many extrinsic factors more than the merits of the language itself. Otherwise JavaScript wouldn't be at the top of the list.

          • By instig007 2025-08-0519:52

            > In 2020, Haskell was close to Kotlin, Rust, and Dart. [...] In 2025, Kotlin and Dart have pulled ahead into the second bucket, but Haskell is moving in the other direction.

            > It's behind Perl, and Perl itself is not exactly doing great.

            Your comment reminded me of gamers who "play games" by watching "letsplay" videos on youtube.

        • By epolanski 2025-08-0514:215 reply

          And yet, while PHPs, Javas, and even nicher/newer languages like Kotlin, Clojure or Scala have plenty of killer software (software that makes it worth learning a language just to use that library/framework) Haskell has none after 30 years. Zero.

          Mind you, I know and like Haskell, but its issues are highly tied to the failure of the simple haskell initiative (also the dreadful state of its tooling).

          • By gylterud 2025-08-0515:431 reply

            There are lots of great libraries, like repa, servant, megaparsec, gloss, yampa… as well as bindings to lots of standard stuff. I consider parsing to be one of Haskell’s killer strengths and I would definitely use it to write a compiler.

            There is also some popular user facing software like Pandoc, written in Haskell. And companies using it internally.

            • By epolanski 2025-08-0517:251 reply

              The only non irrelevant compiler ever written in Haskell is for another borderline dead project: Elm.

              • By gylterud 2025-08-0519:11

                What are you on about?

                The Agda compiler, Pugs, Cryptol, Idris, Copilot (not that copilot you are thinking of), GHC, PureScript, Elm…

                These might not be mainstream, but are (or were for Pugs, but the others are current) important within their niche.

          • By milutinovici 2025-08-0514:44

            It has PostgREST, which is the heart of supabase

          • By dmead 2025-08-0514:411 reply

            yea I agree. haskell was my primary language for several years in the 00s. it's since had almost zero industry uptake. Don't come at me with jane street or the one off startup.

            I thought for a while I'd be able to focus on getting jobs that liked haskell. it never happened.

            • By chriswarbo 2025-08-0515:561 reply

              I certainly wouldn't focus on getting a Haskell job. Yet they are out there; e.g. my current job is Haskell, and happens to be in the same sector (public transport) as my last job (which was mostly Scala).

              Also, I've found Haskell appropriate for some one-off tasks over the years, e.g.

              - Extracting a load of cross-referenced data from a huge XML file. I tried a few of our "common" languages/systems, but they all ran out of memory. Haskell let me quickly write something efficient-enough. Not sure if that's ever been used since (if so then it's definitely tech debt).

              - Testing a new system matched certain behaviours of the system it was replacing. This was a one-person task, and was thrown away once the old system was replaced; so no tech debt. In fact, this was at a PHP shop :)

              • By dmead 2025-08-0516:361 reply

                Yea of course, its not really the focus for me either way. my point was that how great haskell seemed in grad school didn't match up with the real world interest.

                I use spark for most tasks like that now. Guido stole enough from haskell that pyspark is actually quite appealing for a lot of these tasks.

                • By instig007 2025-08-0520:061 reply

                  > Guido stole enough from haskell that pyspark is actually quite appealing for a lot of these tasks.

                  He didn't do his homework. Guido or whoever runs things around the python language committee nowadays didn't have enough mental capacity to realize that the `match` must be a variable bindable expression and never a statement to prevent type-diverging case branches. They also refuse to admit that a non-blocking descriptor on sockets has to be a default property of runtime and never assigned a language syntax for, despite even Java folks proving it by example.

          • By myko 2025-08-0518:551 reply

            I will not stand for this Xmonad slander

            • By dmead 2025-08-0716:49

              ahhh i was there, 3000 years ago when it was announced in IRC.

          • By instig007 2025-08-0519:541 reply

            > also the dreadful state of its tooling

            this is plain and unsubstantiated FUD

            > Haskell has none after 30 years

            > I know Haskell

            I doubt it

            • By tome 2025-08-069:182 reply

              FYI instig007 is not part of the Haskell community but seems to occasionally lambast people about Haskell, risking giving the Haskell community a bad name: https://news.ycombinator.com/item?id=44199980

              • By instig007 2025-08-0720:18

                > but seems to occasionally lambast people about Haskell

                If you are to add community notes to my comments, at least add the part that clarifies that I only lambast incompetence and lies.

                > risking giving the Haskell community a bad name

                as opposed to those that spread FUD, I suppose? It's not the first time I'm asking this question, so what's your take on people who inflate their credibility by telling lies about the tech they clearly don't know?

              • By dmead 2025-08-0616:23

                rar

        • By dmead 2025-08-0514:412 reply

          it's easy to learn and speak latin as well.

          • By gylterud 2025-08-0515:45

            Yes, but there is very little modern latin slang. While GHC gives us great new extensions of Haskell quite often.

          • By 1-more 2025-08-0516:591 reply

            Latin never paid my mortgage. Helped on the SATs though.

            • By dotancohen 2025-08-0712:381 reply

              I think the point is that there is hardly anybody to speak Latin to, much as there are hardly any actual jobs in Haskell.

              • By 1-more 2025-08-0717:51

                And yet I, who am kinda dumb, have managed to find some of each. There's hope out there, beloved Latinists and Haskellers. Never let the fire in your heart burn out.

  • By bapak 2025-08-055:087 reply

    Meanwhile the JS world has been waiting for 10 years for this proposal, which is still in stage 2 https://github.com/tc39/proposal-pipeline-operator/issues/23...

    • By avaq 2025-08-057:483 reply

      Not only have we been waiting for 10 years, the most likely candidate to go forward is not at all what we wanted when the proposal was created:

      We wanted a pipe operator that would pair well with unary functions (like those created by partial function application, which could get its own syntax), but that got rejected on the premise that it would lead to a programming style that utilizes too many closures[0], and which could divide the ecosystem[1].

      Yet somehow PHP was not limited by these hypotheticals, and simply gave people the feature they wanted, in exactly the form it makes most sense in.

      [0]: https://github.com/tc39/proposal-pipeline-operator/issues/22... [1]: https://github.com/tc39/proposal-pipeline-operator/issues/23...

      • By lexicality 2025-08-058:234 reply

        Am I correct in my understanding that you're saying that the developers of the most widely used JS engine saying "hey we can't see a way to implement this without tanking performance" is a silly hypothetical that should be ignored?

        • By jeroenhd 2025-08-0510:041 reply

          With JS' async/await system basically running on creating temporary closures, I don't think things will change all that much to be honest.

          Furthermore, I don't see why engines should police what is or isn't acceptable performance. Using functional interfaces (map/forEach/etc.) is slower than using for loops in most cases, but that didn't stop them from implementing those interfaces either.

          I don't think there's that much of a performance impact when comparing

              const x = fun1(abc);
              const y = fun2(x);
              const z = fun3(y);
              fun4(z);
          
          and

              abc |> fun1 |> fun2 |> fun3 |> fun4
          
          especially when you end up writing code like

              fun1(abc).then( (x) => fun2(x)).then( (y) => fun3(y)).then((z) => fun4(z))
          
          when using existing language features.

          • By ufo 2025-08-0517:30

            The problem they were discussion in the linked Github issue are pipelines where the functions receive more than one argument.

                const x = fun1(a, 10)
                const y = fun2(x, 20)
                const z = fun3(y, 30)
            
            In this case the pipeline version would create a bunch of throwaway closures.

                a |> ((a) => fun1(a, 10))
                  |> ((x) => fun2(x, 20))
                  |> ((y) => fun3(y, 30))

        • By avaq 2025-08-058:451 reply

          They can't implement function application without tanking performance? I find that hard to believe. Especially considering that function application is already a commonly used (and, dare I say: essential) feature in the language, eg: `Math.sqrt(2)`.

          All we're asking for is the ability to rewrite that as `2 |> Math.sqrt`.

          What they're afraid of, my understanding goes, is that people hypothetically, may start leaning more on closures, which themselves perform worse than classes.

          However I'm of the opinion that the engine implementors shouldn't really concern themselves to that extent with how people write their code. People can always write slow code, and that's their own responsibility. So I don't know about "silly", but I don't agree with it.

          Unless I misunderstood and somehow doing function application a little different is actually a really hard problem. Who knows.

          • By nilslindemann 2025-08-0510:421 reply

            Your simple example (`2 |> Math.sqrt`) looks great, but when the code gets more complex, then the advantage of the pipe syntax is less obvious. For example,

                foo(1, bar(2, baz(3)), 3)
            
            becomes something like

                1 (2, (3 |> baz) > bar), 3 |> foo
            
            or

                (3 |> baz) |> (2, % |> bar) |> (1, %, 3 |> foo)
                
            
            That looks like just another way to write a thing in JavaScript, and it is not easier to read. What is the advantage?

            • By avaq 2025-08-0511:251 reply

              Uhm, don't do it, then. That's like arguing that the addition of the ternary operator is a bad one because not all if/else blocks look better when translated into it.

              The goal is to linearlize unary function application, not to make all code look better.

              • By sir_eliah 2025-08-0513:581 reply

                I think the commenter meant that once the new syntax is approved and adopted by the community, you have no choice to not use the syntax. You'll eventually change your project and will be forced to deal with reviewing this code.

                • By dotancohen 2025-08-0712:48

                  _You_ might not use it, but somebody's going to send a PR, or some LLM is going to spit it out, or some previous maintainer put it in there, or will be in some tutorial, or it will be in some API documentation.

                  You are going to have to deal with it as a mess at some point. One of the downfalls of perl was the myriad of ways of doing any particular thing. We would laugh that perl was a write only language - nobody knew all the little syntax tricks.

        • By hajile 2025-08-0515:452 reply

          Closures won over OOP in Javascript a long time ago (eg, React switching from classes to functions + closures), but they still keep trying to force garbage like private variables on the community.

          Loads of features have been added to JS that have worse performance or theoretically enable worse performance, but that never stopped them before.

          Some concrete (not-exhaustive) examples:

          * Private variables are generally 30-50% slower than non-private variables (and also break proxies).

          * let/const are a few percent slower than var.

          * Generators are slower than loops.

          * Iterators are often slower due to generating garbage for return values.

          * Rest/spread operators hide that you're allocating new arrays and objects.

          * Proxies cause insane slowdowns of your code.

          * Allowing sub-classing of builtins makes everything slow.

          * BigInt as designs is almost always slower than the engine's inferred 31-bit integers.

          Meanwhile, Google and Mozilla refuse to implement proper tail calls even though they would INCREASE performance for a lot of code. They killed their SIMD projects (despite having them already implemented) which also reduced performance for the most performance-sensitive applications.

          It seems obvious that performance is a non-issue when it's something they want to add and an easy excuse when it's something they don't want to add.

          • By tracker1 2025-08-0516:252 reply

            I wish I could upvote this more than once. I really liked the F# inspired pipe operator proposal and even used it a bit when I used to lean on 6to4/babel more, but it just sat and languished forever it seems. I can't really think of any other language feature I've seen since that I would have wanted more. The new Temporal being one exception.

            • By hajile 2025-08-0517:03

              It seems like none of the really good language proposals have much traction. Proper Tail Calls have been in the language for TEN YEARS now, but v8 and Spidermonkey still violate the spec and refuse to implement for no good reason.

              Record/tuple was killed off despite being the best proposed answer for eliminating hidden class mutation, providing deep O(1) comparisons, and making webworkers/threads/actors worth using because data transfer wouldn't be a bottleneck.

              Pattern matching, do expressions, for/while/if/else expressions, binary AST, and others have languished for years without the spec committee seemingly caring that these would have real, tangible upsides for devs and/or users without adding much complexity to the JIT.

              I'm convinced that most of the committee is completely divorced from the people who actually use JS day-to-day.

            • By jedwards1211 2025-08-0522:58

              I think it’s mainly because they struggled to get consensus on which syntax to go with for pipelines, since people were divided into three different camps. I wish they would just standardize all three options with a slightly different operator for each one

          • By int_19h 2025-08-0516:531 reply

            > * Rest/spread operators hide that you're allocating new arrays and objects.

            Only in function calls, surely? If you're using spread inside [] or {} then you already know that it allocates.

            • By hajile 2025-08-0517:39

              It used to be said that "Lisp programmers know the value of everything and the cost of nothing."

              This applies to MOST devs today in my experience and doubly to JS and Python devs as a whole largely due to a lack of education. I'm fine with devs who never went to college, but it becomes an issue when they never bothered to study on their own either.

              I've worked with a lot of JS devs who have absolutely no understand of how the system works. Allocation and garbage collection are pure magic. They also have no understanding of pointers or the difference between the stack and heap. All they know is that it's the magic that makes their code run. For these kinds of devs, spread just makes the object they want and they don't understand that it has a performance impact.

              Even among knowledgeable devs, you often get the argument that "it's fast enough" and maybe something about optimizing down the road if we need it. The result is a kind of "slow by a thousand small allocations" where your whole application drags more than it should and there's no obvious hot spot because the whole thing is one giant, unoptimized ball of code.

              At the end of the day, ease of use, developer ignorance, and deadline pressure means performance is almost always the dead-last priority.

        • By nobleach 2025-08-0518:11

          I know that was the reasoning for the Records/Tuples proposal being shot down. I haven't dug too deeply into the pipeline operators other than to get a feel for both proposals.

          Most of the more interesting proposals tend to languish these days. When you look at everything that's advanced to Stage 3-4, it's like. "ok, I'm certain this has some amazing perf bump for some feature I don't even use... but do I really care?"

      • By xixixao 2025-08-058:24

        I guess partially my fault, but even in the article, you can see how the Hack syntax is much nicer to work with than the functional one.

        Another angle is “how much rewriting does a change require”, in this case, what if I want to add another argument to the rhs function call. (I obv. don’t consider currying and point-free style a good solution)

      • By WorldMaker 2025-08-0517:15

        I am wondering if PHP explicitly rejecting Hack-style pipes (especially given the close ties between PHP and Hack, and that PHP doesn't have partial application, but JS does, sort of, though its UX could be improved) will add leverage to the F#-style proposal over the Hack-style.

        It may be useful data that the TC-29 proposal champions can use to fight for the F# style.

    • By wouldbecouldbe 2025-08-055:178 reply

      It’s really not needed, syntax sugar. With dots you do almost the same. Php doesn’t have chaining. Adding more and more complexity doesn’t make a language better.

      • By chilmers 2025-08-059:582 reply

        I'm tired of hearing the exact same arguments, "not needed", "just syntax sugar", "too much complexity", about every new syntax feature that gets added to JS. Somehow, once they are in the language, nobody's head explodes, and people are soon using them and they become uncontroversial.

        If people really this new syntax will make it harder to code in JS, show some evidence. Produce a study on solving representative tasks in a version of the language with and without this feature, showing that it has negative effects on code quality and comprehension.

        • By robertlagrant 2025-08-0511:44

          Presumably it's up to the change proposers to produce said study showing the opposite.

        • By 38 2025-08-0512:19

          [dead]

      • By bapak 2025-08-055:241 reply

        Nothing is really needed, C89 was good enough.

        Dots are not the same, nobody wants to use chaining like underscore/lodash allowed because it makes dead code elimination impossible.

        • By pjmlp 2025-08-057:301 reply

          K&R C was good enough for UNIX System V, why bother with C89.

          • By boobsbr 2025-08-059:141 reply

            K&R C was the apex, we've just been going downhill since.

            • By lioeters 2025-08-0515:47

              This but unironically.

      • By troupo 2025-08-055:40

        > With dots you do almost the same.

        Keyword: almost. Pipes don't require you to have many different methods on every possible type: https://news.ycombinator.com/item?id=44794656

      • By hajile 2025-08-0516:10

        Chaining requires creating a class and ensuring everything sticks to the class and returns it properly so the chain doesn't blow up. As you add more options and do more stuff, this becomes increasingly hard to write and maintain.

        If I'm using a chained library and need another method, I have to understand the underlying data model (a leaky abstraction) and also must have some hack-ish way of extending the model. As I'm not the maintainer, I'm probably going to cause subtle breakages along the way.

        Pipe operators have none of these issues. They are obvious. They don't need to track state past the previous operator (which also makes debugging easier). If they need to be extended, look at your response value and add the appropriate function.

        Composition (whether with the pipe operator or not) is vastly superior to chaining.

      • By Martinussen 2025-08-059:37

        When you say chaining, do you mean autoboxing primitives? PHP can definitely do things like `foo()->bar()?->baz()`, but you'd have to wrap an array/string yourself instead of the methods being pulled from a `prototype` to use it there.

      • By purerandomness 2025-08-0510:34

        If your team prefers not to use this new optional feature, just enable a PHPStan rule in your CI/CD pipeline that prevents code like this getting merged.

      • By EGreg 2025-08-055:221 reply

        It’s not really chaining

        More like thenables / promises

        • By wouldbecouldbe 2025-08-055:251 reply

          It looks like chaining, but with possibility of adding custom functions?

          • By bapak 2025-08-055:592 reply

            It's chaining without having to vary the return of each function. In JS you cannot call 3.myMethod(), but you could with 3 |> myMethod

            • By cyco130 2025-08-056:191 reply

              It requires parentheses `(3).myMethod()` but you can by monkey patching the Number prototype. Very bad idea, but you absolutely can.

              • By senfiaj 2025-08-1014:59

                You can just add extra dot: `3..myMethod()`.

            • By EGreg 2025-08-0514:20

              Not only that

              In chaining, methods all have to be part of the same class.

              In C++ we had this stuff ages ago, it’s called abusing streaming operators LMAO

      • By te_chris 2025-08-056:21

        Dots call functions on objects, pipe passes arguments to functions. Totally missing the point.

    • By fergie 2025-08-058:26

      Good- the [real world examples of pipes in js](https://github.com/tc39/proposal-pipeline-operator?tab=readm...) are deeply underwhelming IMO.

    • By epolanski 2025-08-0514:25

      Sadly they went obsessing over pipes with promises which don't fit the natural flow.

      Go explain them that promises already have a natural way to chain operations through the "then" method, and don't need to fit the pipe operator to do more than needed.

    • By lacasito25 2025-08-056:34

      in typescript we can do this

      let res res = op1() res = op2(res.op1) res = op3(res.op2)

      type inference works great, and it is very easy to debug and refactor. In my opinion even more than piping results.

      Javascript has enough features.

    • By 77pt77 2025-08-0511:29

      Best you'll git will be 3 new build systems and 10 new frameworks

    • By defraudbah 2025-08-058:36

      do not let me start on monads in golang...

      both are going somewhere and super popular though

  • By donatj 2025-08-0511:1811 reply

    I had this argument in the PHP community when the feature was being discussed, but I think the syntax is much more complicated to read, requiring backtracking to understand. It might be easier to write.

    Imagine you're just scanning code you're unfamiliar with trying to identify the symbols. Make sense of inputs and outputs, and you come to something as follows.

        $result = $arr
            |> fn($x) => array_column($x, 'values')
            |> fn($x) => array_merge(...$x)
            |> fn($x) => array_reduce($x, fn($carry, $item) => $carry + $item, 0)
            |> fn($x) => str_repeat('x', $x);
    
    Look at this operation imaging your reading a big section of code you didn't write. This is embedded within hundreds or thousands of lines. Try to just make sense of what "result" is here? Do your eyes immediately shoot to its final line to get the return type?

    My initial desire is to know what $result is generally speaking, before I decide if I want to dive into its derivation.

    It's a string. To find that out though, you have to skip all the way to the final line to understand what the type of $result is. When you're just making sense of code, it's far more about the destination than the path to get there, and understanding these require you to read them backwards.

    Call me old fashioned, I guess, but the self-documentating nature of a couple variables defining what things are or are doing seems important to writing maintainable code and lowering the maintainers' cognitive load.

        $values = array_merge(...array_column($arr, 'values'));
        $total  = array_reduce($values, fn($carry, $item) => $carry + $item, 0);
    
        $result = str_repeat('x', $x);

    • By hombre_fatal 2025-08-0515:272 reply

      The problem with intermediate assignment is that they pollute your scope.

      You might have $values and then you transform it into $b, $values2, $foo, $whatever, and your code has to be eternally vigilant that it never accidentally refers to $values or any of the intermediate variables ever again since they only existed in service to produce some downstream result.

      Sometimes this is slightly better in languages that let you repeatedly shadow variables, `$values = xform1($values)`, but we can do better.

      That it's hard to name intermediate values is only a symptom of the problem where many intermediate values only exist as ephemeral immediate state.

      Pipeline style code is a nice general way to keep the top level clean.

      • By donatj 2025-08-0522:02

        If PHP scoped to blocks, it would be less of an issue, you could just wrap your procedural code in curly braces and call it a day

            {
                $foo = 'bar'; // only defined in this block
            }
        
        I use this reasonably often in Go, I wish it were a thing in PHP. PHP allows blocks like this but they seem to be noops best I can tell.

      • By procaryote 2025-08-0515:561 reply

        Put it in a function and the scope you pollute is only as big as you make it.

        • By hombre_fatal 2025-08-0516:082 reply

          Functions also pollute the scope the same way. And you don't want to be forced to extract a function that is never reused just to hide intermediate values; you should only have to extract a function when you want the abstraction.

          The pipeline transformation specifically lets you clean this up with functions at the scope of each ephemeral intermediate value.

          • By ckdot 2025-08-0520:01

            You definitely want to extract code into functions, even if you don’t need to reuse it. Functions names are documentation. And you reduce the mental load from those who read the code.

          • By skoskie 2025-08-0519:37

            That’s why we have classes and namespaces.

            Anyone can write good or bad code. Avoiding new functionality and syntax won’t change that.

    • By drakythe 2025-08-0514:121 reply

      I don't disagree with you. I had trouble reading the examples at first. But what immediately struck me is this syntax is pretty much identical to chaining object methods that return values.

        $result = $obj->query($sqlQuery)->fetchAll()[$key]
      
      so while the syntax is not my favorite, it at least maintains consistency between method chaining and now function chaining (by pipe).

      • By 8n4vidtmkvmk 2025-08-0517:03

        Speaking of query builders, we no longer have to guess whether it's mutating the underlying query object or cloning it with each operation. That's another big win for pipe IMO.

    • By altairprime 2025-08-0515:19

      It reads well to me, as someone familiar with Perl map and jq lambda. But I would syntactic sugar it rather more strongly using a new `|=>` operator implying a distributive `|>` into its now-inferred-and-silent => arguments:

          $result = $arr |> fn($x) |=>
              array_column($x, 'values'),
              array_merge(...$x),
              array_reduce($x, fn($carry, $item) => $carry + $item, 0),
              str_repeat('x', $x);
      
      As teaching the parser to distribute `fn($x) |=> ELEM1, ELEM2` into `fn($x) => ELEM1 |> fn($x) => ELEM2 |> …` so that the user isn’t wasting time repeating it is exactly the sort of thing I love from Perl, and it’s more plainly clear what it’s doing — and in what order, without having to unwrap parens — without interfering with any successive |> blocks that might have different needs.

      Of course, since I come from Perl, that lends itself well to cleaning up the array rollup in the middle using a reduce pipe, and then replacing all the words with operators to make incomprehensible gibberish but no longer needing to care about $x at all:

          $result = $arr |> $x:
              ||> 'values'
              |+< $i: $x + $i
              |> str_repeat('x', $x);
      
      Which rolls up nicely into a one-liner that is completely comprehensible if you know that | is column, + is merge, < is reduce, and have the : represent the syntactic sugar for conserving repetitions of fn($x) into $x using a stable syntax that the reduce can also take advantage of:

          $result = $arr |> $x: ||> 'values' |+< $i: $x + $i |> str_repeat('x', $x);
      
      Which reads as a nice simple sentence, since I grew up on Perl, that can be interpreted at a glance because it fits within a glance!

      So. I wouldn’t necessarily implement everything I can see possible here, because Perl proved that the space of people willing to parse symbols rather than words is not the complete programmer space. But I do stand by the helpfulness of the switch-like |=> as defined above =)

    • By philjohn 2025-08-0512:441 reply

      This is what a good IDE brings to the table, it'll show that $result is of type string.

      The pipe operator (including T_BLING) was one of the few things I enjoyed when writing Hack at Meta.

      • By xienze 2025-08-0512:541 reply

        > This is what a good IDE brings to the table, it'll show that $result is of type string.

        I think the parent is referring to what the result _means_, rather than its type. Functional programming can, at times, obfuscate meaning a bit compared to good ol’ imperative style.

        • By 8n4vidtmkvmk 2025-08-0516:59

          If you want meaning, don't call your variable "result"

    • By epolanski 2025-08-0514:23

      You're conflating different concepts: familiarity and simplicity.

      I don't find the pipe alternative to be much harder to read, but I'd also favour the first one.

      In any case, we shouldn't judge software and it's features on familiarity.

    • By sandbags 2025-08-0511:23

      I don’t disagree with your reasoning but I would have thought this pipe would be in an appropriately named function (at least that’s how I’d use it in Elixir) to help understand the result.

    • By troupo 2025-08-0513:121 reply

      > I think the syntax is much more complicated to read, requiring backtracking to understand.

      Same as with `array_merge(...array_column($arr, 'values'));` or similar nested function calls.

      > Imagine you're just scanning code you're unfamiliar with trying to identify the symbols. Make sense of inputs and outputs, and you come to something as follows.

      We don't have to imagine :) People working in languages supporting pipes look at similar code all day long.

      > but the self-documentating nature of a couple variables defining what things are or are doing seems important to writing maintainable code

      Pipes do not prevent you from using a couple of variables.

      In your example I need to keep track of $values variable, see where it's used, unwrap nested function calls etc.

      Or I can just look at the sequential function calls.

      What PHP should've done though is just pass the piped value as the first argument of any function. Then it would be much cleaner:

        $result = $arr
          |> array_column('values')
          |> array_merge()
          |> array_reduce(fn($carry, $item) => $carry + $item, 0)
          |> fn($x) => str_repeat('x', $x);
      
      I wouldn't be surprised if that's what will eventually happen

      • By WorldMaker 2025-08-0517:09

        The article addresses this pretty well.

        Quick summary: Hack used $$ (aka T_BLING) as the implicit parameter in a pipeline. That wasn't accepted as much fun as the name T_BLING can be. PHP looked for a solution and started looking for a Partial Function Application syntax they were happy with. That effort mostly deadlocked (though they hope to return to it) except for syntax some_function(...) for an unapplied function (naming a function without calling it).

        Seems like an interesting artifact of PHP functions not being first class objects. I wish them luck on trying to clean up their partial application story further.

    • By mcaruso 2025-08-0514:48

      People use method chaining all the time and don't have any issue with it? It's equivalent to something like:

          $result = $arr
              ->column('values')
              ->merge()
              ->reduce(fn($carry, $item) => $carry + $item, 0)
              ->repeat('x');
      
      I think this just comes down to familiarity.

    • By tracker1 2025-08-0516:16

      I think it's more a matter of what you're used to. It's simply an operator and syntax that you aren't used to seeing. Like if they added back a character into English that you aren't familiar with and started using it in words that you no longer recognize.

      A lot of people could say the same of the rest/spread syntax as well.

    • By layer8 2025-08-0513:41

      I completely agree about intermediate variables (and with explicit type annotations in a typed language) to make the code more intelligible.

      But maybe also, the pipe syntax would be better as:

          $arr
          |> fn($x) => array_column($x, 'values')
          |> fn($x) => array_merge(...$x)
          |> fn($x) => array_reduce($x, fn($carry, $item) => $carry + $item, 0)
          |> fn($x) => str_repeat('x', $x)
          |= $result;

    • By int_19h 2025-08-0516:49

      It's no different than chained property accesses or method calls, or more generally nested expressions. Which is to say, if you overuse it, you hamper readability, but if you have a named result for every single operation, it is also hard to read because it introduces too much noise.

HackerNews