Remix.run Logo
donatj 7 days ago

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);
hombre_fatal 7 days ago | parent | next [-]

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.

donatj 7 days ago | parent | next [-]

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.
procaryote 7 days ago | parent | prev [-]

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

hombre_fatal 7 days ago | parent [-]

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.

ckdot 7 days ago | parent | next [-]

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.

skoskie 7 days ago | parent | prev [-]

That’s why we have classes and namespaces.

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

drakythe 7 days ago | parent | prev | next [-]

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).
8n4vidtmkvmk 7 days ago | parent [-]

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.

altairprime 7 days ago | parent | prev | next [-]

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 =)

philjohn 7 days ago | parent | prev | next [-]

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.

xienze 7 days ago | parent [-]

> 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.

8n4vidtmkvmk 7 days ago | parent [-]

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

epolanski 7 days ago | parent | prev | next [-]

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.

sandbags 7 days ago | parent | prev | next [-]

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.

troupo 7 days ago | parent | prev | next [-]

> 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
WorldMaker 7 days ago | parent [-]

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.

mcaruso 7 days ago | parent | prev | next [-]

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.
tracker1 7 days ago | parent | prev | next [-]

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.

layer8 7 days ago | parent | prev | next [-]

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;
int_19h 7 days ago | parent | prev [-]

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.