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.

gapminder_2007 |> data() + point + x(gdp) + y(life)
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp

“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)
Asia Europe Africa Americas Oceania 0 200 400 600 Count 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)
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp

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)`.
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp

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)
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp

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)
10K 20K 30K 40K 50K 72 74 76 78 80 82 Life Gdp

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)
10K 20K 30K 40K 50K 72 74 76 78 80 82 Life Gdp

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:

actuals: all 5 rows
year sales
2019 120
2020 135
2021 128
2022 152
2023 168
forecast: all 3 rows
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") + point
plot(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, bigrquery

gog 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-cli
pkgload::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/gog

The 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.