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.
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.
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.
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.
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).
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.
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?
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)
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
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:
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:
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 =)
> 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.
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.
> 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:
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.
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.
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.
While I appreciate the effort and like the approach in general, in this use case I really would prefer extensions / extension functions (like in Kotlin[1]) or an IEnumerable / iterator approach (like in C#).
$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.
;
The advantage is that pipes don't care about the type of the return value.
Let's say you add a reduce in the middle of that chain. With extension methods that would be the last one you call in the chain. With pipes you'd just pipe the result into the next function
Well, while traits might be a workaround for certain use cases, simple arrays with scalar data types could not be extended via traits.
While I know that there are Collection classes in Symfony, Laravel, etc., I'm not a huge fan of wrapping a PHP array with a class to get method chaining, even with generators.
cannot be solved with traits. Additionally, I think traits should be used very carefully and they do not have this many use cases that aren't a code smell to me.
While converting arrays to collection-object is a suitable option that does work, it would feel much more "native", if there were extension methods for Iterable / Traversable.
Correctly naming things is one of the harder challenges in computer programming. Putting effort into naming intermediates that you're going to throw out is a waste. Plus, the more variables you introduce, the more likely you'll accidentally re-use a variable name somewhere down the line.
With PHP allowing variable initialization in one branch but not the other, and continuing execution by default when an undeclared variable is passed, declaring more variables can lead to an annoying class of bugs that would require significant (breaking) changes to the core language to completely eliminate.
Introducing a new variable every single line adda a bunch of cognitive load compared to the pipe operator.
It's much easier skim with the pipe operator and it's more robust too (for example reordering is a pain with variables, it's easy to introduce errors).
The main problem with this approach, as someone who programs in PHP daily, is it pollutes the scope. That makes debugging much, much harder - you lose track of variables, and the current state of the program is complicated. IMO, if a value is a throwaway, like an intermediate, we shouldn't be able to use it. So method chaining or nesting function calls prevents them. Then, when we break after these functions, we can't see fake values. It also prevents someone in the future mutating the throwaway values. Someone could easily insert logic or a call that mutates something in the middle of this and breaks the chain.
One way this is prevented in PHP is just using functions. But then you have functions just for the sake of scope, which isn't really what they're for. That introduces other annoyances.
Such variable threading tends to be harder to skim through in production code, the intermediates become noise that's harder to filter out than a repeated symbol like |>.
Preferably you should also be sure that the functions are compatible with the data type going in and only rarely have to break it to dump data mid-chain. If you expect that kind of erroring it's likely a builder-chain with -> is a better alternative and do logging in the methods.
It tends to work a little better in Elixir, because you very rarely have to include one-off lambdas in your pipeline. The standard library functions are designed to work with the pipeline operator, where the thing you probably want to thread through is usually the first argument.
I'm surprised that the example requires lambdas... What's the purpose of the `|> foo(...)' syntax if the function has to take exactly one operand? Why is it necessary to write this?
$arr
|> fn($x) => array_column($x, 'tags')
Why doesn't this work?
$arr
|> array_column(..., 'tags')
And when that doesn't work, why doesn't this work?
Apparently "foo(...)" is just the PHP syntax for a function reference, according to the "first-class callable" RFC [1] linked from the article.
So where in Python you would say e.g.
callbacks = [f, g]
PHP requires the syntax
$callbacks = [f(...), g(...)];
As for the purpose of the feature as a whole, although it seems like it could be replaced with function composition as mentioned at the end of the article, and the function composition could be implemented with a utility function instead of dedicated syntax, the advantage of adding these operators is apparently [2] performance (fewer function calls) and facilitating static type-checking.
It is to interject the chained value at the right position in the function.
They write that elixir has a slightly fancier version, it is likely around this, they mean (where elixir has first class support for arity > 1 functions)
But the example suggests that it can't interject the chained value at the right position; if that was the case, the example would've been written as `|> array_column('tags', ...)`.
I really believe the thing PHP needs the most is a rework of string / array functions to make them more consistent and chain able. Now they are at least chainable.
I'm not a fan of the ... syntax though, especially when mixed in the same chain with the spread operator
The syntax could be improved by allowing you to omit the (...) part entirely for single argument functions and using currying for functions that need additional arguments. So you would end up with something like:
$result = $arr
|> select_column('tags') // Gets an array of arrays
|> fn($x) => array_merge(...$x) // Flatten into one big array
|> array_unique // Remove duplicates
|> array_value // Reindex the array.
Agree, the ... syntax feels confusing when each fn($x) in the example uses $x as the name of its argument.
My initial instinct would be to write like this:
`$result = $arr
|> fn($arr) => array_column($arr, 'tags') // Gets an array of arrays
|> fn($cols) => array_merge(...$cols)`
Which makes me wonder how this handles scope. I'd imagine the interior of some chained function can't reference the input $arr, right? Does it allow pass by reference?
> because that's the way the underlying C libraries also worked
I feel like this is a weak defence of the internally inconsistent behaviour. As someone who has been programming with PHP for over twenty years now, most of them professionally, I still cannot remember the needle/haystack order in these functions, I thank intellisense for keeping me sane here.
As evident with this pipe operator, or with for example Attributes, PHP does not need to religiously follow the C way of doing things, so why not improve it instead of dismissing it as "it is the way it is because that is the way it was"?
"A major limitation of the pipe operator is that all the callables in the chain must accept only one required parameter.
For built-in functions, if the function does not accept any parameters, it cannot be used in a chain. For user-land PHP functions, passing a parameter to a function that does not accept any parameters does not cause an error, and it is silently ignored.
With the pipe operator, the return value of the previous expression or the callable is always passed as the first parameter to the next callable. It is not possible to change the position of the parameter."
In the light of these limitations I would not call the Elixir implementation "slightly fancier".
I'm not so sure I'll be upgrading my local PHP version just for this but it's nice that they are adding it, I'm sure there is a lot of library code that would look much better if rewritten into this style.
RHS can also be a lambda too:
|> is not a syntactic sugar but is actually defined in the standard library as: For function composition, F# provides >> (forward composition) and << (backward composition), defined respectively as: We can use them to build reusable composed functions: 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.It is indeed a shame that F# never became a first class citizen.
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.
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.
It's usually called operator because it uses an infix notation.
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...
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)
It may be useful data that the TC-29 proposal champions can use to fight for the F# style.
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.
Dots are not the same, nobody wants to use chaining like underscore/lodash allowed because it makes dead code elimination impossible.
Keyword: almost. Pipes don't require you to have many different methods on every possible type: https://news.ycombinator.com/item?id=44794656
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.
More like thenables / promises
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.
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.
both are going somewhere and super popular though
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.
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.
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.
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:
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: 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 =)
The pipe operator (including T_BLING) was one of the few things I enjoyed when writing Hack at Meta.
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.
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.
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:
I wouldn't be surprised if that's what will eventually happenQuick 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.
A lot of people could say the same of the rest/spread syntax as well.
But maybe also, the pipe syntax would be better as:
1: https://kotlinlang.org/docs/extensions.html#extension-functi...
``` val arr = ... val result = arr .let { column(it, "tags") .let { merge(it) } .let { unique(it) } .let { values(it) } ```
You add function references for single-argument functions too:
``` arr.let(::unique) // or (List<>::unique), depends on the function ```
all without adding a special language construct.
Let's say you add a reduce in the middle of that chain. With extension methods that would be the last one you call in the chain. With pipes you'd just pipe the result into the next function
The use-case in the article could still be solved easier with extension methods in my opinion :-)
While I know that there are Collection classes in Symfony, Laravel, etc., I'm not a huge fan of wrapping a PHP array with a class to get method chaining, even with generators.
cannot be solved with traits. Additionally, I think traits should be used very carefully and they do not have this many use cases that aren't a code smell to me.While converting arrays to collection-object is a suitable option that does work, it would feel much more "native", if there were extension methods for Iterable / Traversable.
With PHP allowing variable initialization in one branch but not the other, and continuing execution by default when an undeclared variable is passed, declaring more variables can lead to an annoying class of bugs that would require significant (breaking) changes to the core language to completely eliminate.
It's much easier skim with the pipe operator and it's more robust too (for example reordering is a pain with variables, it's easy to introduce errors).
One way this is prevented in PHP is just using functions. But then you have functions just for the sake of scope, which isn't really what they're for. That introduces other annoyances.
Preferably you should also be sure that the functions are compatible with the data type going in and only rarely have to break it to dump data mid-chain. If you expect that kind of erroring it's likely a builder-chain with -> is a better alternative and do logging in the methods.
Readability is mostly matter of habit. One reads easily what he/she is used to read.
Is it though? I don't think so.
So where in Python you would say e.g.
PHP requires the syntax As for the purpose of the feature as a whole, although it seems like it could be replaced with function composition as mentioned at the end of the article, and the function composition could be implemented with a utility function instead of dedicated syntax, the advantage of adding these operators is apparently [2] performance (fewer function calls) and facilitating static type-checking.[1] https://wiki.php.net/rfc/first_class_callable_syntax
[2] https://wiki.php.net/rfc/function-composition#why_in_the_eng...
https://wiki.php.net/rfc/partial_function_application_v2https://wiki.php.net/rfc/pipe-operator-v3#rejected_features
Deleted Comment
They write that elixir has a slightly fancier version, it is likely around this, they mean (where elixir has first class support for arity > 1 functions)
I really believe the thing PHP needs the most is a rework of string / array functions to make them more consistent and chain able. Now they are at least chainable.
I'm not a fan of the ... syntax though, especially when mixed in the same chain with the spread operator
My initial instinct would be to write like this:
`$result = $arr
Which makes me wonder how this handles scope. I'd imagine the interior of some chained function can't reference the input $arr, right? Does it allow pass by reference?Edit: And you can pass by reference:
Never done it in practice, though, not sure if there are any footguns besides the obvious hazards in remote mutation.string functions use (haystack, needle) and array functions use (needle, haystack)
because that's the way the underlying C libraries also worked
array_filter takes (arr, callback)
https://www.php.net/manual/en/function.array-filter.php
array_map takes (callback, arr)
https://www.php.net/manual/en/function.array-map.php
I feel like this is a weak defence of the internally inconsistent behaviour. As someone who has been programming with PHP for over twenty years now, most of them professionally, I still cannot remember the needle/haystack order in these functions, I thank intellisense for keeping me sane here.
As evident with this pipe operator, or with for example Attributes, PHP does not need to religiously follow the C way of doing things, so why not improve it instead of dismissing it as "it is the way it is because that is the way it was"?
There isn't a good reason for PHP to have inherited C's issues here.
How it that consistent?
For built-in functions, if the function does not accept any parameters, it cannot be used in a chain. For user-land PHP functions, passing a parameter to a function that does not accept any parameters does not cause an error, and it is silently ignored.
With the pipe operator, the return value of the previous expression or the callable is always passed as the first parameter to the next callable. It is not possible to change the position of the parameter."
https://php.watch/versions/8.5/pipe-operator
In the light of these limitations I would not call the Elixir implementation "slightly fancier".
I'm not so sure I'll be upgrading my local PHP version just for this but it's nice that they are adding it, I'm sure there is a lot of library code that would look much better if rewritten into this style.