R (4.6) Module 5 – Pipes, functionals and iteration
The native pipe |> and anonymous functions
Rewrite nested R calls as pipelines with the native pipe |>, send a value to any argument with _, write \(x) functions and compare |> with %>%.
What you will learn
- Rewrite nested calls as pipelines with the native pipe |>
- Use the _ placeholder for non-first arguments and for extraction
- Write anonymous functions with \(x), and choose a named function when it reads better
- Explain how |> differs from magrittr's %>%
Before you start
What you’ll learn · Before you start
What you will learn
- Rewrite nested calls as pipelines with the native pipe |>
- Use the _ placeholder for non-first arguments and for extraction
- Write anonymous functions with \(x), and choose a named function when it reads better
- Explain how |> differs from magrittr's %>%
Before you start
On this page
round(mean(as.numeric(weights), na.rm = TRUE), 1) computes 68.2 from five weights typed into a form, but it has to
be read from the inside out: the first step to run, as.numeric(), sits in the middle, and the 1 that round()
needs comes at the very end, separated from round( by the rest of the line. The native pipe writes the same call in
the order the steps run: weights |> as.numeric() |> mean(na.rm = TRUE) |> round(1).
On recorded runs of R 4.6.1 you will rewrite nested calls as pipelines, send a value to an argument other than the
first with the placeholder _, write small functions in place with \(x), and read code written with the older
%>% of the magrittr package, which follows different rules from |> in four places that matter when you convert one
into the other.
A pipeline runs its steps in the order you read them
x |> f(y) means f(x, y): the native pipe: R's built-in |> operator, in R 4.1.0 and later. The parser rewrites x |> f(y) as f(x, y), so the value on the left becomes the first argument of the call on the right. takes the value on its left and passes it to
the call on its right as that call’s first argument. A chain passes each result on to the next call, so
x |> f() |> g() is g(f(x)), and the steps appear on the page in the order R runs them. Step through the weights:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The value on the left. weights holds five texts from a form, "61.5", "70.2", NA, "58.9" and "82.0". It is the left-hand side of the first pipe.
- The first call. The first pipe passes weights to as.numeric() as its first argument, so this step runs as.numeric(weights) and the texts become numbers.
- Own arguments stay put. The next pipe puts those numbers in front of mean()'s own argument, so the call is mean(numbers, na.rm = TRUE). The missing weight is dropped and the mean is 68.15.
- The last call. The last pipe makes round(68.15, 1), which is 68.2. The 1 belongs to round(), and in the pipeline it is written right beside it.
- The same call, nested. Without pipes the same three calls are round(mean(as.numeric(weights), na.rm = TRUE), 1), read from the inside out, with the 1 for round() at the far end.
Each stage received the value from the stage above it as its first argument, and kept its own arguments where you
wrote them: na.rm = TRUE stayed with mean() and the 1 with round(). The run computes the nested call and the
pipeline, then the pipeline again with one step per line, the layout most R code uses:
from session import run, version
version()
# R reads pipe_rewrite.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f pipe_rewrite.R
""") Output
# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f pipe_rewrite.R
> # Weights in kg from a clinic's form, read as text, one of them missing.
> weights <- c("61.5", "70.2", NA, "58.9", "82.0")
> # Nested: the first step to run, as.numeric(), is the innermost call.
> round(mean(as.numeric(weights), na.rm = TRUE), 1)
[1] 68.2
> # Piped: the steps read left to right, in the order they run.
> weights |> as.numeric() |> mean(na.rm = TRUE) |> round(1)
[1] 68.2
> # One step per line: a line that ends with |> continues on the next one.
> weights |>
+ as.numeric() |>
+ mean(na.rm = TRUE) |>
+ round(1)
[1] 68.2
> # The same pipeline without its last step, to see what reaches round().
> weights |> as.numeric() |> mean(na.rm = TRUE)
[1] 68.15
> # A line that starts with |> cannot continue the line before it.
> tryCatch(parse(text = c("weights", " |> as.numeric()")), error = function(e) writeLines(conditionMessage(e)))
<text>:2:3: unexpected '|>'
1: weights
2: |>
^
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pipe_rewrite_session.py
All three give 68.2. A line that ends with |> is not finished, so R reads on to the next line; the console shows
that with its + prompt. The next line drops the final step and prints 68.15, the value round() received. Cutting a
pipeline short like this is the quickest way to see what reaches any step.
Putting |> at the start of a line
weights on one line and |> as.numeric() on the next cannot be joined: the first line is already a complete
expression, so the second starts with |> and is a syntax error, “unexpected ‘|>’”, as the last line of the run
shows. End each line with |> instead, so the expression is visibly unfinished.
Checkpoint
Check: Reading and writing a pipeline
The pipe is rewritten by the parser before anything runs
quote(weights |> mean(na.rm = TRUE)) prints mean(weights, na.rm = TRUE): there is no pipe left in the expression.
The help page of the pipe says that pipe operations are implemented as a syntax transformation, so x |> f(y) is
parsed as f(x, y). The pipe is not a function that runs; the parser rewrites your code into the nested call, and R
then runs that call as if you had typed it. Follow three lines from the parser to the evaluator:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Typed with a pipe. You type weights |> mean(na.rm = TRUE). Before any of it runs, the parser reads the whole line.
- Rewritten. The parser moves weights into the call as its first argument. The expression it builds is mean(weights, na.rm = TRUE), with no pipe left in it, which is what quote() printed.
- Run as usual. The evaluator receives an ordinary call and runs it as if you had typed it nested, so the pipe costs nothing at run time and needs no package.
- Not a call. c(1, 4) |> sqrt has a name, not a call, on its right. The parser stops with "The pipe operator requires a function call as RHS", and nothing runs.
- Laziness is kept. The last line pipes stop() into a function that ignores its argument. The rewritten call passes stop() as that argument, R computes an argument only when it is used, and so nothing stops.
Three things follow from the rewrite, and the run shows each:
from session import run, version
version()
# R reads pipe_syntax.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f pipe_syntax.R
""") Output
# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f pipe_syntax.R
> # The parser rewrites a pipe into an ordinary call, before anything runs.
> quote(weights |> mean(na.rm = TRUE))
mean(weights, na.rm = TRUE)
> quote(weights |> as.numeric() |> mean(na.rm = TRUE) |> round(1))
round(mean(as.numeric(weights), na.rm = TRUE), 1)
> # So a pipeline and the nested call are one and the same expression.
> identical(quote(w |> as.numeric() |> mean()), quote(mean(as.numeric(w))))
[1] TRUE
> # parse_message() returns "parsed", or the message of the parse error.
> parse_message <- function(code) tryCatch({ str2lang(code); "parsed" }, error = conditionMessage)
> # The right-hand side must be a call: sqrt is a name, sqrt() is a call.
> parse_message("c(1, 4) |> sqrt")
[1] "The pipe operator requires a function call as RHS (<input>:1:12)"
> parse_message("c(1, 4) |> sqrt()")
[1] "parsed"
> # Syntactically special functions, such as + and {, are refused.
> parse_message("x |> `+`(1)")
[1] "function '+' not supported in RHS call of a pipe (<input>:1:6)"
> parse_message("x |> { . + 1 }")
[1] "function '{' not supported in RHS call of a pipe (<input>:1:6)"
> # A function written in place must be wrapped in parentheses and called.
> parse_message("x |> function(v) v + 1")
[1] "function 'function' not supported in RHS call of a pipe (<input>:1:6)"
> parse_message("x |> (function(v) v + 1)()")
[1] "parsed"
> # Laziness still applies: the piped value is computed only if the function uses it.
> stop("this is never computed") |> (function(...) "the piped value was not used")()
[1] "the piped value was not used"
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pipe_syntax_session.py
- A pipeline costs nothing extra.
identical()found the piped and the nested expression to be the same, so they run the same code at the same speed, and|>needs no package, only R 4.1.0 or later. - The right-hand side must be a call.
c(1, 4) |> sqrtstopped the parser with “The pipe operator requires a function call as RHS”;sqrt()with its brackets parsed. Syntactically special functions, such as+and{, are refused even when written as a call, and so is a function written in place unless it is wrapped in parentheses and called,(function(v) v + 1)(), a form the section on anonymous functions uses. - Arguments are still lazy. The last line piped
stop()into a function that never uses its argument, and nothing stopped: the rewritten call passes the piped expression as an argument, and R computes an argument only when the function first uses it, as the help page points out.
The messages came from str2lang(), which parses one line of text into an expression without running it, wrapped in
tryCatch() so that each error becomes a message to print. Written into a script, the same mistake stops R before any
tryCatch() could catch it, and how much of the script runs first depends on how the script is run:
from session import run, version
version()
# The same script run two ways: Rscript reads it one expression at a time, source() parses all of it first.
run("""
!Rscript parse_late.R
!R --vanilla --quiet -e 'source("parse_late.R")'
""") Output
# R version 4.6.1 -- "Happy Hop"
$ Rscript parse_late.R
Line 2 ran.
Error in sum :
The pipe operator requires a function call as RHS (<input>:1:27)
Execution halted
[exit status 1]
$ R --vanilla --quiet -e 'source("parse_late.R")'
> source("parse_late.R")
Error in sum :
The pipe operator requires a function call as RHS (<input>:3:27)
Calls: source
Execution halted
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 parse_late_session.py
Rscript reads a file one expression at a time, so line 2 printed its message before line 3 stopped the script.
source() parses the whole file before it runs anything, so the same file printed nothing but the error, whose
3:27 is the line and column of the mistake.
Checkpoint
Check: The pipe as a rewrite by the parser
A placeholder sends the value to a named argument
d |> lm(weight ~ visit) stops with an error saying that a formula cannot be turned into a data frame. lm() takes
the formula first and the data second, so the plain pipe made d the formula and moved the formula into data.
Since R 4.2.0 the pipe placeholder: The _ in x |> f(data = _): it marks the named argument that receives the piped value instead of the first one. It may appear once, and since R 4.3.0 it may also start an extraction such as _$coef. _ fixes that: name the argument that should receive the value and
pass _ to it, as in d |> lm(weight ~ visit, data = _). Watch where d goes in both calls:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The plain pipe. d |> lm(weight ~ visit) passes d as the first argument. In lm() that is formula, so d becomes the formula and weight ~ visit is pushed into data.
- The error. lm() cannot use a formula as its data, and stops with an error about turning a formula into a data frame. The message names the symptom, not the pipe.
- The placeholder. d |> lm(weight ~ visit, data = _) names the argument that gets d. The parser turns it into lm(weight ~ visit, data = d), and the formula stays first.
- The model. lm() fits the line. Weight falls 1.10 kg a visit from 73.45 kg, and the model's printed Call shows data = d, where the parser put the data frame.
- An extraction. The placeholder can also start an extraction. |> _$coefficients[["visit"]] reads one number from the model, the slope, -1.1.
The rules are short. The placeholder must be given to a named argument, it may appear once, and since R 4.3.0 it may
also head a chain of extractions with $, [[, [ or @, such as _$coefficients[["visit"]]. Where _ is not
available, as on R 4.1, naming the arguments before the one you mean works too, because the piped value fills the
first argument not yet matched: d |> lm(formula = weight ~ visit) sends d to data. The run tries each form:
from session import run, version
version()
# R reads placeholder.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f placeholder.R
""") Output
# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f placeholder.R
> # Weight at four visits of a small study.
> d <- data.frame(visit = c(1, 2, 3, 4), weight = c(72.4, 71.1, 70.3, 69.0))
> # lm() takes the formula first and the data second, so the plain pipe sends d to the wrong argument.
> tryCatch(d |> lm(weight ~ visit), error = conditionMessage)
[1] "cannot coerce class ‘\"formula\"’ to a data.frame"
> # Name the argument that should get the value, and pass the placeholder _.
> d |> lm(weight ~ visit, data = _)
Call:
lm(formula = weight ~ visit, data = d)
Coefficients:
(Intercept) visit
73.45 -1.10
> # The placeholder can head a chain of extractions (R 4.3.0 and later).
> d |> lm(weight ~ visit, data = _) |> _$coefficients[["visit"]]
[1] -1.1
> # Both pipes are rewritten into ordinary calls.
> quote(d |> lm(weight ~ visit, data = _) |> _$coefficients[["visit"]])
lm(weight ~ visit, data = d)$coefficients[["visit"]]
> # The placeholder must be a named argument, and it may appear once.
> parse_message <- function(code) tryCatch({ str2lang(code); "parsed" }, error = conditionMessage)
> parse_message("d |> lm(weight ~ visit, _)")
[1] "pipe placeholder can only be used as a named argument (<input>:1:6)"
> parse_message("d |> merge(x = _, y = _)")
[1] "pipe placeholder may only appear once (<input>:1:6)"
> # Without _, name the arguments before it: the value fills the first one left.
> d |> lm(formula = weight ~ visit) |> coef()
(Intercept) visit
73.45 -1.10
> # A slip that gives no error: grepl() takes the pattern first.
> fruits <- c("apple", "kiwi", "mango")
> fruits |> grepl("a")
[1] FALSE
Warning message:
In grepl(fruits, "a") :
argument 'pattern' has length > 1 and only the first element will be used
> fruits |> grepl("a", x = _)
[1] TRUE FALSE TRUE
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 placeholder_session.py
The model’s Call: line shows data = d: the parser rewrote the pipeline into lm(weight ~ visit, data = d), as
quote() printed, and lm() shows that call with its first argument named. The slope read through _ is -1.1 kg a
visit. An unnamed _ and a second _ both stopped at parse time with their own messages.
Piping into a function that takes the data second
fruits |> grepl("a") returns one FALSE and a warning, not three answers: it became grepl(fruits, "a"), so the
three fruit names were used as the pattern. The warning shows the rewritten call. Write
fruits |> grepl("a", x = _), or name the pattern, grepl(pattern = "a"), and the value goes to x.
These pieces arrived in three releases, so code that uses one needs at least that version of R:
- R 4.1.0
The native pipe and the function shorthand new syntax
x |> f(y) is parsed as f(x, y), always filling the first argument, and \(x) x + 1 is parsed as function(x) x + 1.
- R 4.2.0
The placeholder for a named argument new syntax
x |> f(y, data = _) sends x to data instead of the first argument. The placeholder must be given to a named argument and may appear only once on the right-hand side.
- R 4.3.0
The placeholder as the head of an extraction new syntax
x |> _$coef[[2]] reads from the piped value with $, [[, [ or @. Parse errors of pipes also became classed errors that give the line and column.
The timeline has 3 events, from R 4.1.0 to R 4.3.0.
Checkpoint
Check: The placeholder _ and extractions
Anonymous functions are written where they are used
sapply(prices, \(p) p * 1.18) adds 18 % tax to four prices without naming a function: \(p) p * 1.18 is an
anonymous function: A function used without being given a name, written where it is needed, such as \(x) x * 2 passed to sapply(). \(x) is short for function(x), in R 4.1.0 and later., written where it is needed and passed straight to
sapply(). The help page of function says the shorthand \(x) x + 1 is parsed as function(x) x + 1; it arrived
with the pipe in R 4.1.0. Step through the four calls sapply() makes:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The first price. sapply() takes the first price, 250, and calls the function with it as p.
- The first result. The body, p * 1.18, gives 295, and the function returns it. sapply() keeps it as the first result.
- The second price. The same function is called again, now with p = 1200. Nothing from the first call is left over in it.
- The second result. 1200 * 1.18 is 1416. The two other prices, 89 and 640, follow in the same way and give 105.02 and 755.2.
- One vector. With every result a single number, sapply() returns them as one numeric vector, 295.00 1416.00 105.02 755.20, the line the run printed.
The function was called once per price, and sapply() collected the four results into one vector. The run shows the
shorthand, the uses, and one rule from the pipe’s help page: to pipe into a function written in place, wrap it in
parentheses and call it, prices |> (\(p) p[p > 500])().
from session import run, version
version()
# R reads lambdas.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f lambdas.R
""") Output
# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f lambdas.R
> # \(x) is a shorter way to write function(x): the parser makes the same function.
> quote(\(price) price * 1.18)
function(price) price * 1.18
> add_tax <- \(price) price * 1.18
> add_tax(250)
[1] 295
> # Anonymous functions are made for functions that take functions.
> prices <- c(250, 1200, 89, 640)
> sapply(prices, \(p) p * 1.18)
[1] 295.00 1416.00 105.02 755.20
> Filter(\(p) p > 500, prices)
[1] 1200 640
> # To pipe into one, wrap it in parentheses and call it.
> prices |> (\(p) p[p > 500])()
[1] 1200 640
> # A named function is clearer when it is reused, longer than a line, or tested.
> with_tax <- function(price, rate = 0.18) {
+ stopifnot(is.numeric(price), price >= 0)
+ round(price * (1 + rate), 2)
+ }
> sapply(prices, with_tax)
[1] 295.00 1416.00 105.02 755.20
> prices |> with_tax(rate = 0.05)
[1] 262.50 1260.00 93.45 672.00
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lambdas_session.py
quote() printed the shorthand back as function(price) price * 1.18, so the two spellings make the same function.
Anonymous functions suit one short expression used in one place, such as the test that Filter() applies. Give the
function a name instead when you use it twice, when it grows past one line, when it checks its input, or when it
deserves a test: with_tax() in the run does all four, and its name tells the reader what the pipeline step means.
Key idea
Write \(x) for a one-line function used once; as soon as a function is reused, checked or tested, give it a name
with function().
Checkpoint
Check: Anonymous functions and when to name them
The magrittr pipe follows four different rules
Before R 4.1.0, R had no pipe of its own, and R code used %>%, the magrittr pipe: The %>% operator of the magrittr package: a function that passes its left side into the call or the function on its right, with a dot as its placeholder.,
which you will still meet in older scripts, answers and package documentation. It reads like |>, but it is a
function of the magrittr package rather than a rewrite by the parser, and its rules differ in four places: bare
function names, the placeholder, a value used inside a nested call, and braces. The run loads magrittr 2.0.5 and tries
each:
from session import run, version
version()
# R reads magrittr.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f magrittr.R
""") Output
# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f magrittr.R
> library(magrittr)
> packageVersion("magrittr")
[1] ‘2.0.5’
> # %>% accepts a bare function name, which |> refuses.
> c(1, 4, 9) %>% sqrt
[1] 1 2 3
> # Its placeholder is a dot.
> d <- data.frame(visit = c(1, 2, 3, 4), weight = c(72.4, 71.1, 70.3, 69.0))
> d %>% lm(weight ~ visit, data = .) %>% coef()
(Intercept) visit
73.45 -1.10
> # A dot inside a nested call does not stop %>% from adding the value first.
> show_args <- function(...) str(list(...))
> c(5, 6) %>% show_args(length(.))
List of 2
$ : num [1:2] 5 6
$ : int 2
> c(5, 6) %>% show_args(., length(.))
List of 2
$ : num [1:2] 5 6
$ : int 2
> # Braces make one step a small block, with the dot as its value.
> c(9, 16) %>% { . + 1 }
[1] 10 17
> # %>% is a function: the pipeline stays a call to %>%, while |> is rewritten.
> quote(c(1, 4) %>% sqrt())
c(1, 4) %>% sqrt()
> quote(c(1, 4) |> sqrt())
sqrt(c(1, 4))
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 magrittr_session.py
| Rule | Native pipe | %>% (magrittr) |
|---|---|---|
| Needs | R 4.1.0 or later | the magrittr package |
| Right-hand side | a call: sqrt() |
a call or a bare name: sqrt |
| Placeholder | _, once, to a named argument |
., any argument, any number of times |
| Value inside a nested call | not allowed | f(length(.)) still adds the value first |
| Braces | refused | { . + 1 } runs as one step |
| What R runs | the rewritten call, sqrt(x) |
a call to the function %>% |
The fourth row is the one that surprises people. magrittr’s documentation calls it the first-argument rule: when the
dot appears only inside a nested call, the value is still inserted as the first argument, so
c(5, 6) %>% show_args(length(.)) passed two arguments, the vector and its length. The last two lines of the run show
the last row: quote() kept %>% as a call, while the native pipe vanished into sqrt(c(1, 4)).
That last row also shows when something fails. Each script below converts two weights to kilograms with a conversion
factor typed as text by mistake, one through %>% and one through |>:
from session import run, version
version()
# Both scripts stop with an error, and R names the calls that were running when it stopped.
run("""
!R --vanilla --quiet -f error_magrittr.R
!R --vanilla --quiet -f error_native.R
""") Output
# R version 4.6.1 -- "Happy Hop" $ R --vanilla --quiet -f error_magrittr.R > library(magrittr) > # A slip: the conversion factor was typed as text. > to_kg <- function(lb) lb * "0.4536" > c(150, 172) %>% round() %>% to_kg() Error in lb * "0.4536" : non-numeric argument to binary operator Calls: %>% -> to_kg Execution halted [exit status 1] $ R --vanilla --quiet -f error_native.R > # The same slip, in a pipeline written with the native pipe. > to_kg <- function(lb) lb * "0.4536" > c(150, 172) |> round() |> to_kg() Error in lb * "0.4536" : non-numeric argument to binary operator Calls: to_kg Execution halted [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pipe_errors_session.py
Both stop with the same error, but the list of calls that R prints differs: %>% -> to_kg for magrittr, because the
pipe is a function that was running, and only to_kg for the native pipe, whose pipeline had become the nested call
to_kg(round(c(150, 172))) before it ran.
Converting %>% code
Search for a dot used as an argument, data = ., which becomes data = _; for bare function names, which need
(); and for braces or a nested dot, which need a named function or \(x) instead. The tidyverse style guide now
recommends |> over %>%, and the lessons here use |>.
The same guide gives the rule for where a pipeline should end: pipes suit a chain of steps that all work on one main object. When a step needs a second object, or a result in the middle deserves a name of its own, assign it and start a new pipeline from there.
Checkpoint
Check: How the magrittr pipe differs
Practise: predict, then check
Write down each answer before you run anything; the recorded run below then marks your work.
Your predictions
Predict the value of c(16, 25, 36) |> sqrt() |> sum(), the call that
quote(scores |> subset(score > 50) |> nrow()) prints, the slope of effect ~ dose for doses 1, 2, 3 and effects
2.1, 3.9, 6.2 read with _$coefficients[["dose"]], and what c(3, 8, 12) |> (\(v) v[v > 5] * 10)() returns. Then
type the lines into R.
from session import run, version
version()
# R reads practice.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f practice.R
""") Output
# R version 4.6.1 -- "Happy Hop" $ R --vanilla --quiet -f practice.R > # 1. The value of a pipeline: > c(16, 25, 36) |> sqrt() |> sum() [1] 15 > # 2. Where each value goes: > quote(scores |> subset(score > 50) |> nrow()) nrow(subset(scores, score > 50)) > # 3. A placeholder and an extraction: > d <- data.frame(dose = c(1, 2, 3), effect = c(2.1, 3.9, 6.2)) > d |> lm(effect ~ dose, data = _) |> _$coefficients[["dose"]] [1] 2.05 > # 4. A lambda in a pipeline: > c(3, 8, 12) |> (\(v) v[v > 5] * 10)() [1] 80 120
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 practice_session.py
The roots 4, 5 and 6 add up to 15; the parser nests the calls from the last step outwards; the slope is 2.05; and the lambda kept 8 and 12 and multiplied them by 10.
The questions below pick up where your answers so far are weakest. Pick a choice and it tells you at once what it gets right or which rule of the pipe it misses:
Practice that adapts to you
Practise this lesson
15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up: what does the native pipe |> do in R?
It passes the value on its left into the call on its right, as that call’s first argument: x |> f(y) means
f(x, y), and a chain such as x |> f() |> g() means g(f(x)). It has been part of R since 4.1.0 and needs no
package. It is not a function: the parser rewrites the pipeline into the nested call before anything runs, so it adds
no cost at run time, and R’s lazy evaluation of arguments applies as usual. The right-hand side must be a call, so
x |> sqrt is a parse error and x |> sqrt() is right. To send the value to another argument, name that argument
with the placeholder, as in d |> lm(y ~ x, data = _), which needs R 4.2.0 or later.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
Your team wants to replace every %>% in a 3,000-line analysis with the native |>, using find and replace. Which lines will break, which could change their results without an error, how do you find both before running the analysis, and how do you show that the results did not change?
-
Mid to senior
A function in your team's package pipes data into lm() and returns the slope of the model. The package promises to work on R 4.1.2. Which forms of the native pipe may the function use, how do you write its two steps for that version, and how do you keep a later edit from breaking the promise?
Key takeaways
x |> f(y)passesxtof()as its first argument, so a pipeline lists its steps in the order they run and keeps each step’s own arguments beside it.- The parser rewrites a pipeline into the nested call before anything runs, so
|>costs nothing, needs R 4.1.0 or later, keeps lazy evaluation, and refuses a right-hand side that is not a call. - The placeholder
_sends the value to a named argument, once per call, and since R 4.3.0 it can head an extraction such as_$coefficients; naming the arguments before it is the other way to skip the first one. \(x)is short forfunction(x): use it for a one-line function passed once, wrap it in parentheses to pipe into it, and give a function a name when it is reused, checked or tested.- magrittr’s
%>%is a function that accepts bare names, braces and a dot placeholder that may appear anywhere, so converting it to|>means rewriting those steps rather than replacing the symbol.
Terms in this lesson
- anonymous function
- A function used without being given a name, written where it is needed, such as \(x) x * 2 passed to sapply(). \(x) is short for function(x), in R 4.1.0 and later.
- magrittr pipe
- The %>% operator of the magrittr package: a function that passes its left side into the call or the function on its right, with a dot as its placeholder.
- native pipe
- R's built-in |> operator, in R 4.1.0 and later. The parser rewrites x |> f(y) as f(x, y), so the value on the left becomes the first argument of the call on the right.
- pipe placeholder
- The _ in x |> f(data = _): it marks the named argument that receives the piped value instead of the first one. It may appear once, and since R 4.3.0 it may also start an extraction such as _$coef.
References
- Forward Pipe Operator (pipeOp) (The R Core Team)
- Function Definition (function, and the \(x) shorthand) (The R Core Team)
- R News: changes in R 4.1.0 (|> and \(x)), 4.2.0 and 4.3.0 (the placeholder _) (The R Core Team)
- Parse R Expressions (str2lang) (The R Core Team)
- Apply a Function over a List or Vector (sapply) (The R Core Team)
- magrittr: a forward-pipe operator for R (The magrittr authors (tidyverse))
- The tidyverse style guide: pipes (The tidyverse team (Posit))
Report a problem with this lesson
Kept only in this browser. Your Learn progress