rfc:pipe-operator-v3
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
rfc:pipe-operator-v3 [2025/02/09 03:56] – Revise introduction crell | rfc: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 pipe operator has a deliberately low binding order, so that most surrounding operators will execute first. | The pipe operator has a deliberately low binding order, so that most surrounding operators will execute first. | ||
Line 76: | Line 78: | ||
</ | </ | ||
- | One notable | + | One notable |
<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. | ||
+ | </ | ||
+ | |||
+ | Also of note, PHP's comparison operators ('' | ||
+ | |||
+ | <code php> | ||
+ | // Without the parens here, PHP would try to | ||
+ | // compare the strlen closure against an integer, which is nonsensical. | ||
+ | $res1 = (' | ||
</ | </ | ||
Line 96: | Line 105: | ||
Pipe supports any callable syntax supported by PHP. At present, the most common form is first-class-callables (eg, '' | Pipe supports any callable syntax supported by PHP. At present, the most common form is first-class-callables (eg, '' | ||
- | |||
==== References ==== | ==== References ==== | ||
- | As usual, references are an issue. | + | As usual, references are an issue. |
<code PHP> | <code PHP> | ||
Line 122: | Line 130: | ||
For that reason, pass-by-ref callables are disallowed on the right-hand side of a pipe operator. | For that reason, pass-by-ref callables are disallowed on the right-hand side of a pipe operator. | ||
+ | |||
+ | One exception to this is " | ||
==== Syntax choice ==== | ==== Syntax choice ==== | ||
- | The use of '' | + | F#, [[https:// |
+ | |||
+ | ==== Use cases ==== | ||
+ | |||
+ | The use cases for a pipe operator are varied. | ||
+ | |||
+ | For example, here are some code fragments from existing projects of mine that use a user-space pipe implementation. | ||
+ | |||
+ | === From Crell/Serde === | ||
+ | |||
+ | <code php> | ||
+ | use function Crell\fp\afilter; | ||
+ | use function Crell\fp\amap; | ||
+ | use function Crell\fp\explode; | ||
+ | use function Crell\fp\flatten; | ||
+ | use function Crell\fp\implode; | ||
+ | use function Crell\fp\pipe; | ||
+ | use function Crell\fp\replace; | ||
+ | |||
+ | enum Cases implements RenamingStrategy | ||
+ | { | ||
+ | case Unchanged; | ||
+ | case UPPERCASE; | ||
+ | case lowercase; | ||
+ | case snake_case; | ||
+ | case kebab_case; | ||
+ | case CamelCase; | ||
+ | case lowerCamelCase; | ||
+ | |||
+ | public function convert(string $name): string | ||
+ | { | ||
+ | return match ($this) { | ||
+ | self:: | ||
+ | self:: | ||
+ | self:: | ||
+ | self:: | ||
+ | $this-> | ||
+ | implode(' | ||
+ | strtolower(...) | ||
+ | ), | ||
+ | self:: | ||
+ | $this-> | ||
+ | implode(' | ||
+ | strtolower(...) | ||
+ | ), | ||
+ | self:: | ||
+ | $this-> | ||
+ | amap(ucfirst(...)), | ||
+ | implode('' | ||
+ | ), | ||
+ | self:: | ||
+ | $this-> | ||
+ | amap(ucfirst(...)), | ||
+ | implode('' | ||
+ | lcfirst(...), | ||
+ | ), | ||
+ | }; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * @return string[] | ||
+ | */ | ||
+ | protected function splitString(string $input): array | ||
+ | { | ||
+ | $words = preg_split( | ||
+ | '/ | ||
+ | $input, | ||
+ | -1, /* no limit for replacement count */ | ||
+ | PREG_SPLIT_NO_EMPTY /* don't return empty elements */ | ||
+ | | PREG_SPLIT_DELIM_CAPTURE /* don't strip anything from output array */ | ||
+ | ); | ||
+ | |||
+ | return pipe($words, | ||
+ | amap(replace(' | ||
+ | amap(explode(' | ||
+ | flatten(...), | ||
+ | amap(trim(...)), | ||
+ | afilter(), | ||
+ | ); | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | The various imported functions are higher order functions that return a callable suitable for pipe, and the '' | ||
+ | |||
+ | === From Crell/MiDy === | ||
+ | |||
+ | <code php> | ||
+ | class PageData | ||
+ | { | ||
+ | public array $tags { | ||
+ | get => pipe( | ||
+ | array_merge(...$this-> | ||
+ | array_unique(...), | ||
+ | array_values(...), | ||
+ | ); | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * @param array< | ||
+ | */ | ||
+ | public function __construct( | ||
+ | private array $parsedFiles, | ||
+ | ) {} | ||
+ | |||
+ | private function values(string $property): array | ||
+ | { | ||
+ | return array_column($this-> | ||
+ | } | ||
+ | } | ||
+ | |||
+ | class ParsedFile | ||
+ | { | ||
+ | public function __construct(public array $tags) {} | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | In this (simplified from the actual code) example, the '' | ||
+ | |||
+ | <code php> | ||
+ | public array $tags { | ||
+ | get => $this-> | ||
+ | |> fn($tags) => array_merge(...$tags), | ||
+ | |> array_unique(...), | ||
+ | |> array_values(...), | ||
+ | ); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | The single-expression alternative today would be: | ||
+ | |||
+ | <code php> | ||
+ | public array $tags { | ||
+ | get => array_values(array_unique(array_merge(...$this-> | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Which I believe is inarguably worse. | ||
+ | |||
+ | <code php> | ||
+ | public array $tags { | ||
+ | get { | ||
+ | $tags = $this-> | ||
+ | $tags = array_merge(...$tags); | ||
+ | $uniqueTags = array_unique($tags); | ||
+ | return array_values($unique_tags); | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Which is still less readable and less self-evident than the explicit pipe version. | ||
+ | |||
+ | === Shallow calls === | ||
+ | |||
+ | The use of a pipe for function composition also helps to separate closely related tasks so they can be developed and tested in isolation. | ||
+ | |||
+ | <code php> | ||
+ | function loadWidget($id): | ||
+ | { | ||
+ | $record = DB:: | ||
+ | return makeWidget($record); | ||
+ | } | ||
+ | |||
+ | function loadMany(array $ids): array | ||
+ | { | ||
+ | $data = DB:: | ||
+ | $ret = []; | ||
+ | foreach ($data as $record) { | ||
+ | $ret[] = $this-> | ||
+ | } | ||
+ | return $ret; | ||
+ | } | ||
+ | |||
+ | function makeWidget(array $record): Widget | ||
+ | // Assume this is more complicated. | ||
+ | return new Widget(...$record); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | In this code, it is impossible to test '' | ||
+ | |||
+ | By making it easy to chain functions together, however, that can be rebuilt like this: | ||
+ | |||
+ | <code php> | ||
+ | function loadWidget($id): | ||
+ | { | ||
+ | return DB:: | ||
+ | } | ||
+ | |||
+ | function loadMany(array $ids): array | ||
+ | { | ||
+ | return DB:: | ||
+ | } | ||
+ | |||
+ | function makeWidget(array $record): Widget | ||
+ | // Assume this is more complicated. | ||
+ | return new Widget(...$record); | ||
+ | } | ||
+ | |||
+ | $widget = loadWidget(5) |> makeWidget(...); | ||
+ | |||
+ | $widgets = loadMany([1, | ||
+ | </ | ||
+ | |||
+ | And the latter could be further simplified with either a higher-order function (like '' | ||
+ | |||
+ | <code php> | ||
+ | $profit = loadMany([1, | ||
+ | |> fn(array $records) => array_map(makeWidget(...), | ||
+ | |> fn(array $ws) => array_filter(isOnSale(...), | ||
+ | |> fn(array $ws) => array_map(sellWidget(...), | ||
+ | |> array_sum(...); | ||
+ | </ | ||
+ | |||
+ | And again, a few simple higher-order utility functions would eliminate the need for the wrapping closures. | ||
+ | |||
+ | <code php> | ||
+ | $profit = loadMany([1, | ||
+ | |> amap(makeWidget(...)) | ||
+ | |> afilter(isOnSale(...)) | ||
+ | |> amap(sellWidget(...)) | ||
+ | |> array_sum(...); | ||
+ | </ | ||
+ | |||
+ | That neatly encapsulates the entire logic flow of a process in a clear, compact, highly-testable set of operations. | ||
+ | |||
+ | === Pseudo-extension functions === | ||
+ | |||
+ | " | ||
+ | |||
+ | For instance, the above examples included utility functions '' | ||
+ | |||
+ | <code php> | ||
+ | function amap(callable $c): \Closure | ||
+ | { | ||
+ | return fn(array $a) => array_map($c, | ||
+ | } | ||
+ | |||
+ | function afilter(callable $c): \Closure | ||
+ | { | ||
+ | return fn(array $a) => array_filter($a, | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | That allows them to be used, via pipes, in a manner similar to " | ||
+ | |||
+ | <code php> | ||
+ | $result = $array | ||
+ | |> afilter(is_even(...)) | ||
+ | |> amap(some_transformation(...)) | ||
+ | |> afilter(a_filter(...)); | ||
+ | </ | ||
+ | |||
+ | Which is not far off from what it would look like with scalar methods: | ||
+ | |||
+ | <code php> | ||
+ | $result = $array | ||
+ | -> | ||
+ | -> | ||
+ | -> | ||
+ | </ | ||
+ | |||
+ | But can work with //any// value type, object or scalar. | ||
==== Existing implementations ==== | ==== Existing implementations ==== | ||
- | Multiple user-space libraries exist in PHP that attempt to replicate pipe-like or compose-like behavior. | + | Multiple user-space libraries exist in PHP that attempt to replicate pipe-like or compose-like behavior. |
* The PHP League has a [[https:// | * The PHP League has a [[https:// | ||
Line 165: | Line 437: | ||
===== Future Scope ===== | ===== Future Scope ===== | ||
- | There are a number | + | This RFC is deliberately "step 1" |
A [[rfc: | A [[rfc: |
rfc/pipe-operator-v3.1739073369.txt.gz · Last modified: 2025/02/09 03:56 by crell