rfc:pipe-operator-v3

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
rfc:pipe-operator-v3 [2025/02/09 04:57] – Add use case examples crellrfc:pipe-operator-v3 [2025/02/10 04:42] (current) – Note left-associativity explicitly crell
Line 58: Line 58:
  
 ==== Precedence ==== ==== Precedence ====
 +
 +The pipe operator is left-associative.  The left side will be evaluated first, before the right side.
  
 The pipe operator has a deliberately low binding order, so that most surrounding operators will execute first.  In particular, arithmetic operations, null coalesce, and ternaries all have higher binding priority, allowing for the RHS to have arbitrarily complex expressions in it that will still evaluate to a callable.  For example: The pipe operator has a deliberately low binding order, so that most surrounding operators will execute first.  In particular, arithmetic operations, null coalesce, and ternaries all have higher binding priority, allowing for the RHS to have arbitrarily complex expressions in it that will still evaluate to a callable.  For example:
Line 76: Line 78:
 </code> </code>
  
-One notable implication of this is that if a pipe chain is placed within a larger expression, it will likely need to be enclosed in ''()'' or else it will be misinterpreted.+One notable exception is if other binding orders would result in nonsensical semantics In particular:
  
 <code php> <code php>
Line 82: Line 84:
 $x ? $y |> strlen(...) : $z; $x ? $y |> strlen(...) : $z;
  
-// will be interpreted like this: +// Is interpreted like this:
-($x ? $y) |> (strlen(...) : $z); +
- +
-// When what is most likely intended is this:+
 $x ? ($y |> strlen(...)) : $z; $x ? ($y |> strlen(...)) : $z;
 +
 +// As the alternative (processing the ? first) would not be syntactically valid.
 +</code>
 +
 +Also of note, PHP's comparison operators (''=='', ''==='', ''<'', etc.) have a relatively high binding priority.  Therefore, ''|>'' necessarily binds lower than those, as doing otherwise would require rethinking the entire binding order and that is entirely out of scope.  As a result, comparing the result of a pipe to something requires parentheses around the pipe chain.
 +
 +<code php>
 +// Without the parens here, PHP would try to
 +// compare the strlen closure against an integer, which is nonsensical.
 +$res1 = ('beep' |> strlen(...)) == 4;
 </code> </code>
  
Line 121: Line 130:
  
 For that reason, pass-by-ref callables are disallowed on the right-hand side of a pipe operator.  That is, both examples above would error. For that reason, pass-by-ref callables are disallowed on the right-hand side of a pipe operator.  That is, both examples above would error.
 +
 +One exception to this is "prefer-ref" functions, which only exist in the stdlib and cannot be implemented in user-space.  There are a small handful of functions that will accept either a reference or a direct value, and vary their behavior depending on which they get.  When those functions are used with the pipe operator, the value will be passed by value, and the function will behave accordingly.
  
 ==== Syntax choice ==== ==== Syntax choice ====
Line 259: Line 270:
         get => array_values(array_unique(array_merge(...$this->values('tags'))));         get => array_values(array_unique(array_merge(...$this->values('tags'))));
     }     }
-</doe>+</code>
  
 Which I believe is inarguably worse.  A multi-statement version would require: Which I believe is inarguably worse.  A multi-statement version would require:
Line 315: Line 326:
 function loadMany(array $ids): array function loadMany(array $ids): array
 { {
-    return DB::query("something")+    return DB::query("something else");
-    $ret = []; +
-    foreach ($data as $record) { +
-        $ret[] = $this->makeWidget($record); +
-    } +
-    return $ret;+
 } }
  
Line 359: Line 365:
 "Extension functions" are a feature of Kotlin and C# (and possibly other languages) that allow for a function to act as though it is a method of another object.  It has only public-read access, but has the ergonomics of a method.  While not a perfect substitute, pipes do offer similar capability with a little more work. "Extension functions" are a feature of Kotlin and C# (and possibly other languages) that allow for a function to act as though it is a method of another object.  It has only public-read access, but has the ergonomics of a method.  While not a perfect substitute, pipes do offer similar capability with a little more work.
  
-For instance, the above examples included utility functions ''amap()'' and ''afilter()'' Trivial implementations of those functions are as follows.  (More robust versions that handle any iterable are available in the Crell/fp library.)+For instance, the above examples included utility functions ''amap()'' and ''afilter()'' Trivial implementations of those functions are as follows.  (A more robust version that also handles iterables is only slightly more work.)
  
 <code php> <code php>
Line 431: Line 437:
 ===== Future Scope ===== ===== Future Scope =====
  
-There are a number of potential improvements to this feature that have been left for later, as their implementation would be notably more involved than this RFC.  The author believes they would be of a benefit in their own RFCs.+This RFC is deliberately "step 1" of several closely related features to make composition-based code easier and more ergonomic.  It offers benefit on its own, but deliberately dovetails with several other features that are worthy of their own RFCs.
  
 A [[rfc:function-composition|compose operator]] for closures (likely ''+'').  Where pipe executes immediately, compose creates a new callable (Closure) that composes two or more other Closures.  That allows a new operation to be defined simply and easily and then saved for later in a variable.  Because it is "just" an operator, it is compatible with all other language features.  That means, for example, conditionally building up a pipeline is just a matter of throwing ''if'' statements around as appropriate.  The author firmly believes that a compose operator is a necessary companion to pipe, and the functionality will be incomplete without it.  However, while pipe can be implemented trivially in the compile step, a compose operator will require non-trivial runtime work.  For that reason it has been split out to its own RFC. A [[rfc:function-composition|compose operator]] for closures (likely ''+'').  Where pipe executes immediately, compose creates a new callable (Closure) that composes two or more other Closures.  That allows a new operation to be defined simply and easily and then saved for later in a variable.  Because it is "just" an operator, it is compatible with all other language features.  That means, for example, conditionally building up a pipeline is just a matter of throwing ''if'' statements around as appropriate.  The author firmly believes that a compose operator is a necessary companion to pipe, and the functionality will be incomplete without it.  However, while pipe can be implemented trivially in the compile step, a compose operator will require non-trivial runtime work.  For that reason it has been split out to its own RFC.
rfc/pipe-operator-v3.1739077074.txt.gz · Last modified: 2025/02/09 04:57 by crell