Blame | Last modification | View Log | Download | RSS feed
---title: "A Native Pipe Operator"author:- Luke Tierneyoutput:html_document---The `magrittr` pipe operator is now widely used, in particular with thetidyverse. Several other languages have also adopted, or areconsidering adopting, a similar operator.[Discussions](https://github.com/tc39/proposal-pipeline-operator) of[proposals](https://github.com/tc39/proposal-pipeline-operator/wiki)for JavaScript are interesting and also provide links to what is doneor proposed for some other languages.<!-- some other links:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Pipeline_operatorhttps://www.digitalocean.com/community/tutorials/js-pipeline-operatorhttps://medium.com/linebyline/javascripts-new-pipeline-operator-2845bbf88b05-->This may be a good time to consider whether a pipe operator of somesort should be incorporated in the base R language. In addition toallowing a slightly more compact operator notation by adding a newoperator at the parser level, this may provide an opportunity foraddressing some concerns about the current `magrittr` pipe.The idea of including a pipe operator in base R was suggested by JimHester, among other things, in his [DSC 2017talk](https://rawgit.com/jimhester/presentations/master/2017_07_03-DSC2017-Syntax_Extensions_To_R/2017_07_03-DSC2017-Syntax_Extensions_To_R.html).This proposed implementing the pipe as a syntax transformation in theparser. The [originalimplementation](https://github.com/lionel-/r-source/compare/7e2a6a8...2c774a17f4)was from Lionel Henry.The idea of adding a native pipe operator to base R was also raisedmore recently by Antoine Fabri in a [thread on the R-devel mailinglist](https://stat.ethz.ch/pipermail/r-devel/2019-October/078520.html)that contains some interesting discussions.## Some Issues with the `magrittr` Pipe### Implicit Passing of LHS As First ArgumentBy default the `magrittr` pipe operates in a way that can be viewed aspassing the LHS result to the RHS call by inserting it as the firstargument to the call. This is a very simple rule, which makes it easyto understand. But it has to be used with care if named arguments areinvolved. An example where named arguments can help is if `merge` or`left_join` is to be called with the LHS passed as the secondargument:```rY %>% left_join(x = X)```is equivalent to```rleft_join(X, Y)```In other situations this could potentially lead to errors that arehard to recognize. Intentionally taking advantage of this it probablya bad idea. It is not clear how often unintentional use occurs andleads to errors.Another drawback of implicit argument passing is that it makes theactual computation of a pipeline stage harder to understand since aninvisible argument has to be mentally added to the call. This createsadditional cognitive load for debugging and for someone learning touse functions in both a pipe context and a non-pipe context.The LHS placeholder `.` could be used to make the LHS argument morevisible, but, as this is not required, encouraging this would likely lead toinconsistent code with `.` used some of the time and not other times.### Explicit LHS Placeholder Issues`magrittr` does provide the `.` placeholder, primarily for cases wherethe LHS needs to be passed as an argument other than the first. Thechoice of `.`, the smallest glyph available, for identifying the pipestages that are non-standard is less than ideal. For example, seeingwhere `.` is used here is quite hard:```rrename(airports,dest = faa, dest_alt = alt) %>%select(dest, dest_alt) %>%left_join(flights, ., "dest") %>%filter(dest_alt > 6000) %>%lm(arr_delay ~ dep_time, data = .)```Supporting a LHS placeholder in a RHS call raises some issues:- Can the placeholder appear more than once? If so, a substitutionimplementation is not possible since this would cause the LHSexpression to be evaluated more than once.- Can the placeholder appear only as a top-level argument or inside anargument expression? If it can appear within an argument expressionit will need to obey R's scoping rules, which limits implementationoptions.Supporting a LHS placeholder _in addition_ to the implicit firstargument convention, as `magrittr` does, raises the question of whenthe first argument convention is suspended. It appears that `magrittr`suspends the first argument convention only if the placeholder appearsas a top-level argument:```r> 1 %>% c(2)[1] 1 2> 1 %>% c(., 2)[1] 1 2> 1 %>% c(c(.), 2)[1] 1 1 2```### Implementation IssuesThe `magrittr` implementation seems to have more overhead than needed,making stack traces messier than they need to be. Jim Hester's slidesshow an example that illustrates how a pipe implemented as a syntaxtransformation is one way to improve this.The current implementation creates additional referenced to LHSobjects. This may force copying of possibly large objects in pipestages that otherwise could perform mutations in place. Again animplementation that transforms a pipe into a set of nested calls,together with the recent change to reference counting for determiningmutability, can improve this. It is not clear how important thiscurrently is in practice, but there may be cases where it matters now,and it might matter more in the future as more optimizations areintroduced.### Relation to UNIX PipesThe use of the terms _pipe_ and _pipeline_ is unfortunate. It is oftenclaimed that the `magrittr` pipe is just like the UNIX pipe. This isnot true. UNIX pipes are a very different animal: they deal withstreaming data and enable parallel processing of pipe stages. If weever wanted to add streaming data support to R, and we might, then wewould have to use non-standard terminology since the standardpipe/pipeline terms are taken.## Two Possible DirectionsAfter some discussions within R-core prior to useR 2020 I believe weconverged on two possible approaches if we wanted to add a pipe tobase R:1. Implicit passing of the LHS as the first argument, with no placeholder.2. A required placeholder, `_`, that must appear once, and only once,as a top-level argument of the RHS call.Each could be implemented as a syntax transformation in the parser,but could also be implemented in other ways. Each also has somecorner cases that need to be considered.The [`R-syntax` subversionbranch](https://svn.r-project.org/R/branches/R-syntax) providesexperimental implementations of both approaches. The implicit LHS, noplaceholder option is implemented as `|>`; the explicit placeholderoption is implemented as `>>`. Both are implemented in the parser assyntax transformations.### 1. Implicit LHS, No PlaceholderThis is the simplest option. As there is no placeholder, there is noneed to consider issues such as multiple evaluation of the LHS orproper scoping of a placeholder. The LHS expression is simply insertedas the first argument in the RHS call:```r> quote(x |> f(y))f(x, y)```One issue that needs to be addressed is that inserting an argumentinto calls to syntactically special functions, such as `if` or `for`would produce nonsense and needs to be ruled out:```r> x |> for (i in x) yError: function 'for' not supported in RHS call of a pipe```Since the error is coming from the parser it currently may not beproviding as much context information as one might like.Inserting the LHS into a call to `function` would also producenonsense, but instead of signaling an error a RHS `function`expression is converted to a call to the function with the LHS asargument:```r> quote(x |> function(y) 1 + y)(function(y) 1 + y)(x)```This can be used in cases were the LHS needs to be passed assomething other than the first argument:```rmtcars |> subset(cyl == 4) |> function(d) lm(mpg ~ disp, data = d)```Or, with one of the anonymous function shorthands:```rmtcars |> subset(cyl == 4) |> \(d) lm(mpg ~ disp, data = d)```Adapting an example from the `magrittr` vignette:```rcar_data <-mtcars |>subset(hp > 100) |>\(lhs) aggregate(. ~ cyl, data = lhs,FUN = \(var) var |> mean |> round(2)) |>transform(kpl = 0.4251 * mpg) |>```One efficiency drawback of using anonymous functions as pipe stages:They create a binding and so force duplicating on modify. It is notclear how much this matters in realistic settings.Another variation supported by the implementation is that a symbol onthe RHS is interpreted as the name of a function to call with the LHSas argument:```r> quote(x |> f)f(x)```This can be convenient, but can also be dropped.Other RHS expressions are parse errors:```r> x |> 1Error: The pipe operator requires a function call, a symbol, or an anonymous function expression as RHS```Again the error signaled may need some work.### 2. Required Top-Level Unique PlaceholderThis is also conceptually simple, and explainable as a simple syntaxtransformation:```r> quote(x >> f(_))f(x)> quote(mtcars >> subset(_, cyl == 4) >> lm(mpg ~ disp, data = _))lm(mpg ~ disp, data = subset(mtcars, cyl == 4))```The example from the `magrittr` vignette can be written as```rcar_data <-mtcars >>subset(_, hp > 100) >>aggregate(. ~ cyl, data = _,FUN = function(var) var >> mean(_) >> round(_, 2)) >>transform(_, kpl = 0.4251 * mpg) >>```There is no need to rule out syntactically special functions called onthe RHS:```r1 : 3 >> for (y in _) print(y)```It could be useful to allow RHS expressions to not contain a top-levelplaceholder argument. In this case the RHS should be an expressionproducing a function that is called with the LHS as argument. Thiswould allow:```rx |> foo => foo(x)x |> \(v) foo(v) => (\(v) foo(v))(v)x |> foo() => (foo())(v)```This is probably more useful in other languages than for R, but itwould avoid the need to signal an error if there is no top-levelplaceholder. On the other hand, given the current implicit LHS passingconvention, a RHS call without a place holder in R would most likely bean error.Signaling good errors for misuse of the placeholder ischallenging. Checking that one and only one of the top-level argumentsis a placeholder is straightforward. But properly checking at parsetime that the placeholder is not used improperly in non-top-levelposition is not possible (it would involve determining variable scope,which in R can change at runtime). The current implementation rejectsany non-top-level use of the placeholder. This is fine in```r> 1 >> _ + (2 + _)Error in `_` + (2 + `_`) :pipe placeholder must only appear as a top-level argument in the RHS call```but not in```r> 1 >> c(_, function(_) _)Error in c(`_`, function(`_`) `_`) :pipe placeholder must only appear as a top-level argument in the RHS call```Previously using `_` as a variable without backquotes was not allowedby the parser. This is a holdover from the days when `_` was droppedas an assignment operator. This was changed in the `R-syntax` branchto make it easier to implement the placeholder. It may be possible torestore this behavior and to get the parser to enforce that theplaceholder `_` can only appear at top level on the RHS of a pipe(maybe also that it is required and may only appear once). Much alongthe lines of the way `in` can only appear in a `for()` loop. On theother hand, leaving the once-and-only-once-at-top-level check to codemight make it easier to give intelligible error messages.If use of the placeholder anywhere other than as a RHS top-levelargument can't be prevented by the parser, then the choice is to beoverly conservative and signal an error for any placeholder appearingin non-top-level position in a RHS argument, as the implementationcurrently does, or to rely on an unbound variable error at runtime.### TradeoffsThe choice comes down to the simplicity of the implicit, noplaceholder approach, against the advantages in clarity of an explicitand required placeholder. There may be a slight within R-core for thesimplicity of the first approach if one of these were to be taken.## Some Implementation NotesThere are two, essentially independent, things that could be includedin base:- A new operator, say `>>` or `|>`, added to the parser.- An implementation of a pipe function.They are independent: we could provide an implementation under thename `%>%`, or we could provide syntax without an implementation, as wedo with `:=` now.At this point the `%>%` is in such widespread use that there may notbe much benefit in introducing a new and different operator. On theother hand we would need a different operator if we change thesemantics. A downside of changing the parser is that editors that dosyntax highlighting and indentation, and R code that works with parsedata, would need to be updated. A syntax transformation approach alsoneeds to make sure source references are not messed up.We could provide in base an implementation of `%>%` that correspondsto the "implicit LHS, no placeholder" option above. This could stillbuild and evaluate the nested call and gain most of the benefits of aparser implementation. Having an `exec` primitive might also help.