gapminder_2007 |> data() + point + x(gdp) + y(life)46 R
Every sentence in this book so far was written in R, so this chapter adds nothing to the grammar. What it adds is the four questions only R raises: how the two pipes reach data(), which of gog’s words base R already uses, how a database connection arrives, and how the package installs. What a table is, and how one is passed, is in Data. A table is an ordinary R data frame, and needs no package of its own.
Every block marked {r} below was run when this page was built, through the same engine every other chapter uses. The install commands at the end are shown, not run.
46.1 Pipes
R has two pipes in common use, and R users reasonably want to start a plot with a table they already have. The native pipe works, and draws exactly the plot the unpiped form draws.
“Given gapminder 2007: points, x is gdp, y is life.”
That is not a second way of saying it, and gog does not implement it. By the time the engine sees anything, the two forms are the same expression. You can watch R rewrite one into the other, because quote() shows what the parser built rather than what you typed:
quote(gapminder_2007 |> data() + point + x(gdp) + y(life))data(gapminder_2007) + point + x(gdp) + y(life)
Two facts about R combine to make that work, and both have to hold.
The first is precedence. |> binds more tightly than +, so the pipe finishes before the sentence begins: it takes the table into data() and hands the finished result to +. Derivation with * sits between the two, which is where it needs to be, so a transform still binds to its mark first:
gm_all |> data() + bar * count + x(continent)“Given all the gapminder years: bars derived by count, x is continent.”
The second is when it happens. The native pipe is a parser transformation: writing df |> data() produces the code data(df) before any of it runs. That timing decides everything, because data() reads the table’s name off the expression you wrote. The pipe leaves it a plain name, so nothing is lost. Naming the table is the whole job of data() (see Data), and it is what a later layer resolves its bare columns against.
Arguments still land where you would expect, since the piped value fills the first one:
gapminder_2007 |> data(name = "gapminder") + point + x(gdp) + y(life)46.2 What %>% does to the table’s name
The magrittr pipe, %>%, is the one most R code still uses, and here it behaves differently. It is an ordinary function rather than a parser rule, so it evaluates the left side and passes the value along under its own placeholder, .. The name never reaches data():
library(magrittr)
gapminder_2007 %>% data() + point + x(gdp) + y(life)Warning: gog: magrittr's `%>%` replaced this table with its placeholder `.`
before `data()` could read the name, so the table is called `data`. A layer
resolves its bare columns against the nearest table *by name*, so two tables
piped this way collide. Either name it — `data(name = "...")` — or use R's
native pipe, which keeps the name: `df |> data()` reads as `data(df)`.
The plot is right, and for a single table it stays right. What is gone is the name, so gog says so and tells you how to put it back. Either use the native pipe, or name the table yourself:
gapminder_2007 %>% data(name = "gapminder_2007") + point + x(gdp) + y(life)Only the placeholder raises that warning, so it is worth saying what does not raise it. Filtering before data() is the native pipe’s most useful form, and it raises no warning. The expression is not a plain name, but it is still distinct, so gog uses it as the table’s name.
gm_all |> subset(year == 2007 & continent == "Europe") |> data() +
point + x(gdp) + y(life)The table there is called subset(gm_all, year == 2007 & continent == Europe), which is ugly and still works. name = gives the table a shorter name, and is worth using once a second table is involved:
gm_all |> subset(year == 2007 & continent == "Europe") |> data(name = "europe") +
point + x(gdp) + y(life)46.3 Why the name is worth a warning
An unnamed table is fine until there are two of them. A second data() gives a layer its own table, and “nearest table wins” is decided by name, so two tables under one name leave the rule nothing to choose between:
| year | sales |
|---|---|
| 2019 | 120 |
| 2020 | 135 |
| 2021 | 128 |
| 2022 | 152 |
| 2023 | 168 |
| year | sales |
|---|---|
| 2024 | 180 |
| 2025 | 195 |
| 2026 | 210 |
data(actuals, name = "series") + x(year) + y(sales) + line +
data(forecast, name = "series") + point(data(actuals, name = "series") + x(col.year) + y(col.sales) + line +
data(forecast, name = "series") + point)data(actuals, name = "series") + x(:year) + y(:sales) + line +
data(forecast, name = "series") + pointplot(data(actuals, { name: "series" }), x(col.year), y(col.sales), line,
data(forecast, { name: "series" }), point)Error:
! gog: two different tables are both called `series` — a layer resolves its bare columns against the nearest table by name, so one of these can never be reached. Give them distinct names: `data(name = "...")`.
Without that refusal the plot would draw the first table twice and report the second table’s columns as misspellings, which blames the reader for something the binding lost. Two %>% pipes in one expression reach that state, because both tables arrive as ..
46.4 What the pipe does not change
The pipe feeds data(). It does not replace +, and there is no piped form of the rest of the sentence: marks, channels, and transforms compose with + and * as they do everywhere else in this book. gog has no pipe of its own, because that would be a second spelling for something the language already spells.
This section is about R specifically. Julia’s |> is a function-application operator with different behavior, and Python and JavaScript have no pipe of their own, so the question does not arise in the other three chapters.
46.5 Twenty names base R already uses
Twenty of the package’s exports are also names in the packages R attaches for you, and library(gog) warns you about eleven of them. Here are all twenty:
| from | names |
|---|---|
stats |
density line median quantile smooth step |
graphics |
box layout text title |
grDevices |
palette |
utils |
data stack |
base |
jitter max mean min order range sum |
Nine of the twenty cannot affect you, which is why library(gog) leaves them out, and the reason is a rule in R itself. A mark that takes no argument is an object rather than a function, and so is a transform that takes no parameter. When R evaluates a call, it looks for a function of that name and skips any binding that is not one. So base R still answers, even though library(gog) put its own mean in front:
is.function(mean) # gog's `mean` is an object, so a call cannot reach it[1] FALSE
mean(1:10) # and base R's function answers anyway[1] 5.5
That holds for the functions that take another function as an argument, because they resolve the name the same way:
sapply(gapminder_2007["life"], mean) life
67.00742
The other eleven are functions, and a call does reach gog’s: box, data, density, jitter, layout, order, palette, quantile, range, stack and title. Qualify the one you want, and both remain available:
| you write | you get | for base R’s, write |
|---|---|---|
order(population) |
the plot’s sort order | base::order(df$population) |
data(gapminder_2007) |
the plot’s table | utils::data(diamonds) |
density(2) |
the smoothing transform | stats::density(x) |
jitter(0.5) |
the offset transform | base::jitter(x) |
quantile(0.9) |
the 90th percentile transform | stats::quantile(df$gdp) |
range(0.25, 0.75) |
the band transform | base::range(df$gdp) |
stack(share = TRUE) |
the piling transform | utils::stack(df) |
palette("soft") |
the plot’s colors | grDevices::palette() |
title("Life expectancy") |
the plot’s title | graphics::title(main = "…") |
box() |
the box-and-whisker mark | graphics::box() |
range is a function because it takes a band, so the skip rule above does not save you. That is what writing range(0.25, 0.75) costs you. The refusal reads the argument it was given: a vector where a probability belongs means you wanted base R’s, and the message says so.
range(c(3, 1, 4, 1, 5))Error:
! gog: `range()` takes the band's two ends, each one number between 0 and 1, e.g. `range(0.25, 0.75)`. It was given 5 numbers. For the smallest and largest of a vector, gog masks that name: use `base::range()`.
order is the one that breaks the most ordinary code. Sorting a table beside a plot is ordinary work. gog’s order returns an atom, not the row positions that [ expects. Left alone, the failure arrives far from the line that caused it and names neither order nor gog. Whenever the argument is not a bare name, the refusal says which function you reached and how to reach the other one:
gapminder_2007[order(gapminder_2007$population), ]Error:
! gog: `order(gapminder_2007$population)` is not a column name. gog's `order()` takes a bare column, as in `order(population, desc = TRUE)`. To sort a vector, the base function is still there as `base::order(gapminder_2007$population)`.
Qualified, it sorts:
ranked <- gapminder_2007[base::order(-gapminder_2007$population), ]
head(ranked[c("country", "population")], 3) country population
25 China 1318683096
59 India 1110396331
135 United States 301139947
This book does that in several other chapters, for exactly this reason.
Three of the eleven refuse nothing, because nothing separates the two readings. data(mtcars) is well formed in both readings. It returns a plot’s table and no complaint, so write utils::data() when you mean to load a dataset. title("...") is the same case: a string is what both want. So is box(), which both readings accept with no argument at all. The other eight report the mistake, because the argument you passed is one gog would refuse. order is the partial case: it catches order(df$population) and not order(pop), where pop is a bare name holding a vector.
The names stay as they are. A grammar keeps its own vocabulary, which is Law 3, and the question is settled. Renaming order to sort_by would replace a word every reader knows with one only this package uses. Where another package defines a name gog also defines, attach gog after that package, so R finds gog’s name first, or write gog::interval. Every language meets this problem, and each one asks you to solve it differently; the grammar chapter compares them.
46.6 Five spellings that still need a prefix
The nine objects above are safe in ordinary code, and safe inside sapply(), lapply(), tapply(), mapply(), Map(), Reduce() and apply(). Each of those asks R for a function of the name it was handed, which finds base R’s.
Five spellings still fail, for two different reasons. The first is a name that is passed on twice before anyone looks it up:
aggregate(life ~ continent, gapminder_2007, mean)Error in `get()`:
! object 'FUN' of mode 'function' was not found
By the time the lookup happens, the name being searched for is FUN and not mean, so the message names neither mean nor gog. by() and ave(..., FUN = mean) fail the same way. Writing base::mean fixes all three. The data frame form of aggregate() looks the name up one step earlier, so aggregate(gapminder_2007["life"], gapminder_2007["continent"], mean) works unchanged.
The second reason is a name used as a value rather than called. do.call(sum, list(x)) wants a function and receives an object, so write do.call("sum", list(x)) or do.call(base::sum, list(x)). And f <- mean copies the object, so f(x) reports that it could not find a function called f.
That is the whole cost, and it is worth stating as one rule. If you hand one of those nine words to another function by name, and that function hands it on again, write base:: in front of it.
46.7 Reading from a database
query() takes a DBI connection. DBI is R’s database standard, so the same call reaches SQLite, Postgres, MariaDB, BigQuery, and anything with an ODBC driver.
con <- DBI::dbConnect(RSQLite::SQLite(), "sales.db")
query(con, "SELECT status, revenue FROM orders") + bar + x(status) + y(revenue)DBI and its drivers are Suggests, not hard dependencies. A reader who never writes SQL installs neither. If you do write SQL, install DBI and the one driver your database needs:
install.packages(c("DBI", "RSQLite")) # or RPostgres, RMariaDB, odbc, bigrquerygog never opens a connection and never stores a password. You connect the way you already do, and hand the open connection over.
46.8 Getting it
A plot is drawn by the engine, which is a compiled Rust binary, so the R package has to be able to find one. An installed copy carries its own. The package’s configure script bundles the engine at install time, and refuses to install at all rather than leave you a package that cannot draw.
install.packages("gog", repos = c("https://psychometrician.r-universe.dev",
"https://cloud.r-project.org"))That command works today. gog is published on r-universe. On macOS and Windows it builds a binary with the engine inside it, so installing needs no Rust toolchain.
On Linux the same command installs from source, and the source build compiles the engine. That needs Rust and a network connection. If the machine does not have Rust, install it first and then run the command above:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"The build runs once, while the package installs.
The repos argument names two addresses because r-universe is not CRAN. The first address is where gog lives. The second is an ordinary CRAN mirror, and it is there so your other packages still install as usual. gog is not on CRAN, so both addresses are needed.
There is another route, and it installs straight from the source repository:
remotes::install_github("psychometrician/gog", subdir = "r-pkg/gog")A tarball from GitHub carries the engine’s Rust sources, and configure compiles them during the install. So this route needs a Rust toolchain, which the macOS and Windows builds above do not. It also needs the remotes package, which install.packages("remotes") adds if you do not have it. Take it when you want one particular branch or commit rather than the latest build.
You can also work from a checkout. On that route nothing installs other packages for you, so jsonlite and pkgload have to be installed first. Outside base R, gog needs one package, jsonlite. It turns the sentence you write into the JSON that the engine reads. install.packages() and remotes::install_github() both install it along with gog. Loading from a checkout also needs pkgload, which loads a package from a folder instead of from a library. Install both if you do not have them:
install.packages(c("jsonlite", "pkgload"))If one is missing, the step that needs it stops immediately, and the message names the missing package.
Then build the engine once and load the package from where it sits:
cargo build --release -p gog-clipkgload::load_all("r-pkg/gog")Installing from that same checkout is the better test, because it runs configure and proves the bundling works:
R CMD INSTALL r-pkg/gogThe engine is looked for in five places, in this order: the GOG_CLI_PATH environment variable, a target/ build in a surrounding checkout, the copy inside the installed package, gog-cli on your PATH, and one last search upward from the working directory. A checkout is searched before the bundled copy on purpose. Inside a checkout the bundled engine is a leftover build, and using it would give a stale engine to the person editing the engine.