41  R

41.1 Pipes

R has two pipes in common use, and R users reasonably want to start a plot with the table they already have in hand. The short answer: the native pipe works, and gives you exactly the plot you would have written by hand.

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

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 closes before the chain 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

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 matters more than it sounds, 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

41.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 draws that warning, and it is worth being clear about what does not. Filtering on the way in is the pipe’s most useful shape, and it stays silent: the expression you piped is not a plain name, but it is still yours and still distinct, so gog keeps it as the table’s name rather than lecturing you about it.

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 works. name = is available when you would rather read something else, and becomes 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

41.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 that both lost theirs leave the rule nothing to choose between:

data(actuals, name = "series") + x(year) + y(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 = "...")`.

Before the warning existed, that plot did not refuse. It drew the first table twice and reported the second table’s columns as misspellings, which blames the reader for something the binding lost. Two %>% pipes in one expression reached exactly that state, both tables having arrived as ..

41.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 and will not grow one, 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 has no pipe at all, so their chapters will answer the same question their own way.

41.5 Eighteen names base R already uses

library(gog) prints a long list of masked names, and the list is alarming out of proportion to the problem. Eighteen of the package’s exports are also names in the packages R attaches for you:

from names
stats density line median smooth step
graphics box text title
grDevices palette
utils data stack
base jitter max mean min order range sum

Ten of the eighteen cannot hurt you, and the reason is a rule in R itself. Every mark and every value transform is an object, not a function. 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 with gog’s mean sitting closer:

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 eight are functions, and those do take over: box, data, density, jitter, order, palette, 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)
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()

order is the one that used to hurt. Sorting a data frame beside a plot is ordinary work. gog’s order returns an atom, not the row positions that [ expects. So the failure used to arrive far from the line that caused it, and it named neither order nor gog. It now 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 three chapters, for exactly this reason.

Two of the eight refuse nothing, because nothing tells the two intentions apart. 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.

The names stay as they are. A grammar keeps its own vocabulary, which is Law 3 and a decided question (spec §18). Renaming order to sort_by would trade a word every reader knows for one only this package uses. Where another package owns a name you want, attach gog after it so its names win, or qualify it as gog::interval.

All four languages meet this wall, and each one hits it differently. Julia will not choose between two modules that export one name. It raises, and you import the one you meant. Python has no rule about skipping non-functions, so from gog import * really does replace five builtins. Calling one of them raises rather than misbehaving. R is the quiet one of the four, which is why the eight names above refuse out loud. JavaScript has no collision at all, because nothing reaches your scope unless you import it by name.

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

41.7 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, which builds a binary for macOS, Windows and Linux, and every one of them has the engine inside it. So installing needs no Rust toolchain.

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 yet; when it is, the plain install.packages("gog") will work and this line can lose its second half.

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 does need a Rust toolchain, and the r-universe binaries do not. Take it when you want one particular branch or commit rather than the latest build.

You can also work from a checkout. Build the engine once, then 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 outranks the bundled copy on purpose. Inside a checkout the bundled engine is a leftover build artifact, and preferring it is how a stale engine gets served to the person editing the engine.