├── README.md └── rfc └── pipe-operator.txt /README.md: -------------------------------------------------------------------------------- 1 | # Pipeline operator proposal for PHP 7 2 | 3 | This document aims to propose the implementation of the pipeline operator (`|>`) for PHP 7. 4 | 5 | ### Description 6 | 7 | - Associativity: **Left** 8 | - Precedence: **Lowest** 9 | - Token: **T_PIPELINE** 10 | 11 | The pipeline operator applies the function on its right side to the value on the left side. 12 | This makes easy to write complex expressions that are evaluated from left-to-right/top-to-bottom. 13 | Many people find the left-to-right order more readable, especially in cases like this where the 14 | filters are higher-order functions that take parameters. When called with functions that receive 15 | more than 2 parameters, the left operand is applied as the **last** argument. 16 | 17 | #### Example 18 | 19 | ##### Old style 20 | 21 | ```php 22 | function even(int $x): int { return $x % 2 === 0; } 23 | function println($x) { echo $x, PHP_EOL; } 24 | 25 | Seq::each("println", 26 | Seq::map("sqrt", 27 | Seq::filter("even", 28 | range(1, 10) 29 | ) 30 | ); 31 | ``` 32 | 33 | ##### Pipeline operator 34 | 35 | ```php 36 | function even(int $x): int { return $x % 2 === 0; } 37 | function println($x) { echo $x, PHP_EOL; } 38 | 39 | range(1, 10) |> Seq::filter ("even") 40 | |> Seq::map ("sqrt") 41 | |> Seq::each ("println"); 42 | ``` 43 | 44 | #### Implemented in 45 | 46 | - LiveScript 47 | - F# 48 | - Shellscript 49 | - Elixir 50 | - OCaml 51 | 52 | ### Benefits 53 | 54 | - Readability 55 | - No need to nest functions 56 | 57 | ### Problems 58 | 59 | - 1) Parameterization of built-in functions that differ, such as `array_map` and `array_filter`. 60 | 61 | ### Solutions 62 | 63 | - 1) Use a wildcard `$` for parameter allocation: 64 | 65 | ```php 66 | range(1, 10) |> array_filter($, "even") 67 | |> array_map("sqrt", $) 68 | |> array_walk($, "println"); 69 | ``` 70 | 71 | ### Equivalence 72 | 73 | This feature can actually be emulated by using objects and self-return, with fluent-interfaces: 74 | 75 | #### Implementation 76 | ```php 77 | class Seq 78 | { 79 | private $value; 80 | 81 | function __construct(array $xs) 82 | { 83 | $this->value = $xs; 84 | } 85 | 86 | function map(callable $fn) 87 | { 88 | $this->value = array_map($fn, $this->value); 89 | return $this; 90 | } 91 | 92 | function filter(callable $fn) 93 | { 94 | $this->value = array_filter($this->value, $fn); 95 | return $this; 96 | } 97 | 98 | function each(callable $fn) 99 | { 100 | array_walk($this->value, $fn); 101 | return $this; 102 | } 103 | } 104 | ``` 105 | 106 | #### Usage 107 | ```php 108 | (new Seq(range(1, 10))) -> filter("even") 109 | -> map("sqrt") 110 | -> each("println"); 111 | ``` 112 | 113 | #### Practical example 114 | -------------------------------------------------------------------------------- /rfc/pipe-operator.txt: -------------------------------------------------------------------------------- 1 | ====== PHP RFC: Pipe Operator ====== 2 | * Version: 0.3 3 | * Date: 2016-04-29 4 | * Author: Sara Golemon , Marcelo Camargo 5 | * Status: Under Discussion 6 | * First Published at: http://wiki.php.net/rfc/pipe-operator 7 | 8 | ===== Introduction ===== 9 | 10 | A common PHP OOP pattern is the use of method chaining, or what is also known as "Fluent Expressions". So named for the way one method flows into the next to form a conceptual hyper-expression. 11 | 12 | For example, the following shows a SQL query expression built out of component pieces, then executed: 13 | 14 | 15 | $rs = $db 16 | ->select()->fields('id', 'name', 'email') 17 | ->from('user_table') 18 | ->whereLike(['email' => '%@example.com']) 19 | ->orderAsc('id') 20 | ->execute(); 21 | 22 | 23 | This works well enough for OOP classes which were designed for fluent calling, however it is impossible, or at least unnecessarily arduous, to adapt non-fluent classes to this usage style, harder still for functional interfaces. 24 | 25 | While decomposing these expressions to make use of multiple variables is an option, this can lead to reduced readability, polluted symbol tables, or static-analysis defying type inconsistency such as in the following two examples: 26 | 27 | 28 | $config = loadConfig(); 29 | $dic = buildDic($config); 30 | $app = getApp($dic); 31 | $router = getRouter($app); 32 | $dispatcher = getDispatcher($router, $request); 33 | $logic = dispatchBusinessLogic($dispatcher, $request, new Response()); 34 | $render = renderResponse($logic); 35 | $psr7 = buildPsr7Response($render); 36 | $response = emit($psr7); 37 | 38 | 39 | Or: 40 | 41 | 42 | $x = loadConfig(); 43 | $x = buildDic($x); 44 | $x = getApp($x); 45 | $x = getRouter($x); 46 | $x = getDispatcher($x, $request); 47 | $x = dispatchBusinessLogic($x, $request, new Response()); 48 | $x = renderResponse($x); 49 | $x = buildPsr7Response($x); 50 | $response = emit($x); 51 | 52 | 53 | This may lead to error prone code, enforcing reassigment or pollution of the scope for readability. These sorts of chains could also, conceivably, be handled using deeply nested expressions, but the code becomes even more unreadable this way: 54 | 55 | 56 | $dispatcher = getDispatcher(getRouter(getApp(buildDic(loadConfig()))), $request); 57 | $response = emit(buildPsr7Response(renderResponse(dispatchBusinessLogic($dispatcher, $request, new Response())))); 58 | 59 | 60 | This RFC aims to improve code readability by bringing fluent expressions to functional and OOP libraries not originally designed for the task. Several languages already provide support for the pipe operator or, at least, for definining it. Some of the languages that offer support are Elixir, F#, LiveScript and HackLang. 61 | 62 | ===== Proposal ===== 63 | 64 | Introduce the "Pipe Operator" ''|>'', mirroring the method call operator ''->''. This expression acts as a binary operation using the result of the LHS as an input to the RHS expression at an arbitrary point denoted by the additional "Pipe Replacement Variable" expression ''$$''. The result of the RHS expression, after substitution, is used as the result of the operator. 65 | 66 | This feature has already been implemented in HackLang, and the manual page for it may be referenced at: https://docs.hhvm.com/hack/operators/pipe-operator 67 | 68 | ==== PSR7 Example ==== 69 | 70 | Here's the equivalent chain of function calls as demonstrated in the intro section above: 71 | 72 | 73 | $response = loadConfig() 74 | |> buildDic($$) 75 | |> getApp($$) 76 | |> getRouter($$) 77 | |> getDispatcher($$, $request) 78 | |> dispatchBusinessLogic($$, $request, new Response()) 79 | |> renderResponse($$) 80 | |> buildPsr7Response($$) 81 | |> emit($$); 82 | 83 | 84 | ==== File Collection Example ==== 85 | 86 | As an example, consider the following real block of code I wrote while creating a test importer (to migrate HHVM format tests into PHPT format). Please try not to get hung up into whether or not it's "good" PHP code, but rather that it's solving a problem, which is precisely what PHP is designed to do: 87 | 88 | 89 | $ret = 90 | array_merge( 91 | $ret, 92 | getFileArg( 93 | array_map( 94 | function ($x) use ($arg) { return $arg . '/' . $x; }, 95 | array_filter( 96 | scandir($arg), 97 | function ($x) { return $x !== '.' && $x !== '..'); } 98 | ) 99 | ) 100 | ) 101 | ); 102 | 103 | 104 | This block of code is readable, but one must carefully examine the nesting to determine what the initial input it, and what order it traverses the steps involved. 105 | 106 | With this proposal, the above could be easily rewritten as: 107 | 108 | 109 | $ret = scandir($arg) 110 | |> array_filter($$, function($x) { return $x !== '.' && $x != '..'; }) 111 | |> array_map(function ($x) use ($arg) { return $arg . '/' . $x; }, $$) 112 | |> getFileArg($$) 113 | |> array_merge($ret, $$); 114 | 115 | 116 | This, cleary and unambiguously, shows `scandir()` as the initial source of data, that it goes through an `array_filter` to avoid recursion, an `array_map` to requalify the paths, some local function, and finally a merge to combine the result with a collector variable. 117 | 118 | ==== FBShipIt Example ==== 119 | 120 | Also consider [[https://github.com/facebook/fbshipit/blob/a995e82/fb-examples/lib/FBCommonFilters.php-example#L20,L41|the follow segment of code]] which is used in production by FBShipIt to translate and export nearly all of Facebook's Opensource projects to github: 121 | 122 | 123 | return $changeset 124 | |> self::skipIfAlreadyOnGitHub($$) 125 | |> self::stripCommonFiles( 126 | $$, 127 | $config['stripCommonFiles/exceptions'] ?? ImmVector {}, 128 | ) 129 | |> self::stripSubjectTags($$) 130 | |> self::stripCommands($$) 131 | |> self::delinkifyDifferentialURLs($$) 132 | |> self::restoreGitHubAuthor($$) 133 | |> ShipItUserFilters::rewriteSVNAuthor( 134 | $$, 135 | FBToGitHubUserInfo::class, 136 | ) 137 | |> self::filterMessageSections( 138 | $$, 139 | $config['filterMessageSections/keepFields'] 140 | ?? self::getDefaultMessageSectionNames(), 141 | ) 142 | |> self::rewriteMentions($$) 143 | |> self::rewriteReviewers($$) 144 | |> self::rewriteAuthor($$); 145 | 146 | 147 | This presents every step taken by the common filter chain in an easy to follow list of actions. 148 | 149 | ===== Backward Incompatible Changes ===== 150 | 151 | While most ambiguities of `$$` between pipe replacement variable and variable variables are covered in the lexer rule, the following case is not accounted for: 152 | 153 | 154 | $a = 1; 155 | $b = 'a'; 156 | var_dump($$ /* comment */ {'b'}); 157 | // Expected: int(1) 158 | // Actual: Use of $$ outside of a pipe expression 159 | 160 | 161 | This particular quirk of the parser (allowing comments in the middle of a variable-variable-brace-expression) is doubtlessly a rare occurrence in the wild, so the current implementation stopped short of trying to resolve it. 162 | 163 | Potential resolutions: 164 | * Use a less-ambiguous token. `$>`, which mirrors `|>`, is my personal favorite. Downshot: doesn't match HackLang 165 | * Get very creative in the parser. Since '$' '{' expr '}' is handled by the parser, then perhaps '$' '$' should as well. So far, attempts to resolve this result in conflicts. More work may yet yield results. 166 | 167 | Note that HHVM does not handle this case either. Nor, in fact, does it handle mere whitespace between `$$` and `{expr}`, which the attached PHP implementation does. 168 | 169 | **Update:** HackLang is normally supposed to disallow variable-variables, so the use of `$$` was seen as non-conflicting. A bug in the 3.13 implementation of pipe operator meant that variable-variables temporarily wound up working where they should not have. So whatever we propose for Pipe Operator's substitution will face the same issues in both lexers eventually anyhow. 170 | 171 | ===== Proposed PHP Version(s) ===== 172 | 7.2 173 | 174 | ===== Open Issues ===== 175 | See BC issues 176 | 177 | ===== Future Scope ===== 178 | The current proposal limits use of the `$$` to a single replacement per expression. This feature could potentially be expanded to allow multiple uses of `$$` within a single RHS expression. 179 | 180 | ===== Third-party Arguments ===== 181 | 182 | Informal Twitter poll (821 respondents) results: https://twitter.com/SaraMG/status/727305412807008256 183 | 184 | * 62% "Love It" 185 | * 24% "Don't Care" 186 | * 14% "Hate It" 187 | 188 | ==== In favor ==== 189 | 190 | * Produces cleaner, more readable code, in order the things are executed 191 | * Doesn't pollute local symbol table with intermediates of potentially varying types 192 | * Enforces immutability and data transformation, less chances of bugs 193 | 194 | ==== Against ==== 195 | 196 | * The new tokens are inobvious and difficult to google for 197 | * Pipe chaining in other languages follows different rules \\ (e.g. implicit first arg, rather than explicit placeholder) 198 | * Potentially confusing with variable-variables 199 | * No opportunity for error catching/handling 200 | * Can be implemented using intermediate variables 201 | 202 | ===== Proposed Voting Choices ===== 203 | Adopt the Pipe Operator yes/no? Requires a 2/3 + 1 majority. 204 | 205 | ===== Patches and Tests ===== 206 | 207 | https://github.com/php/php-src/compare/master...sgolemon:pipe.operator 208 | --------------------------------------------------------------------------------