This post is one of a series of posts that I am doing about quasi-quotation in R. This post tries to explain quasi-quotation by analogy. Other posts in the series currently are the series introduction and quasi-quotation applications.
Warning: We are embarking on an extended analogy wherein the quasi-quotation tools in base R and the rlang package are compared to recipes.
Consider the following interesting recipe:
This recipe references other recipes. Most recipes aren’t like this, but I think most of us have seen this kind of thing. I want to call it a “meta-recipe.”
There are two fundamentally different types of “ingredients” in this recipe. Chopped butternuts are a normal, basic ingredient, but “White Cake II” is something different. We expect that if we turn to page 636, we will find a recipe for the cake which starts from basic ingredients.
Now imagine that you made this cake and your friend loved it. He asks for the recipe. If you give him the photo above, it would be fundamentally incomplete. One idea is to rewrite the “Maple Butternut Cake” recipe by copy-and-pasting the sub-recipes for the cake and frosting all into one place, so that we have a single self-contained recipe.
Doing this rewriting is the essence of the unquote operator in quasi-quotation.
Before we get to the meta-recipe in code, I want to develop the analogy step-by-step.
Expressions are Recipes
Normally, when you tell R how to bake a cake, it goes right to work.
cake <- flour + sugar + eggs
## Error in eval(expr, envir, enclos): object 'flour' not found
In this example, the code flour + sugar + eggs
is an expression, or a recipe. When we run this, R will immediately try to evaluate the expression and store the result in a new object called cake
. The code above shows an error because when R looked in the pantry for flour, it couldn’t find any.
objects()
## character(0)
The Environment is the Pantry
Lets stock the pantry. We will assign these objects with numbers that might represent the number of calories in a serving.
flour <- 20
eggs <- 50
sugar <- 72
objects()
## [1] "eggs" "flour" "sugar"
Evaluating is Baking
Now let’s try again to bake that cake:
cake <- flour + sugar + eggs
cake
## [1] 142
We have a cake! Apparently it has 142 calories. Not surprising, as cake is very tasty!
The Recipe versus the Cake
The big idea here is you don’t have to immediately bake the cake. In R there are language features and quotation functions which make it possible to write down recipes and bake them at a later time.
In service of this extended comparison, I am going to rename my own quoting function recipe
as follows:
recipe <- rlang::expr
Note that I am using rlang::expr
here, but the basic analogy is equally applicable to base quote
or rlang::quo
. The distinction is in which pantry to look in, and I won’t get into that in this post. Remember:
The recipe is to the cake as an expression is to the result of evaluating the expression.
Anyway, let’s write down the recipe for cake.
cake_recipe <- recipe(flour + sugar + eggs)
cake_recipe
## flour + sugar + eggs
Great, now we have the recipe written down. Note that when we type regular code into R, it immediately evaluates it. But when we type cake_recipe
into the console it just prints the recipe – it doesn’t bake it. In our code snippets, R objects that represent food, like cake
have a numerical value akin to calories, while unevaluated expressions, like cake_recipe
, don’t have a numerical value. Recipes don’t have calories – read as many as you want, but you will still be hungry!
Evaluating is Baking Part 2
Now that we have saved our recipe, let’s bake it. This time I will alias the rlang:::eval_tidy
function as bake
so that I can extend the analogy in the code. Again, a version of this would work equally well with base eval
or rlang::eval_bare
.
bake <- eval
cake2 <- bake(cake_recipe)
cake2
## [1] 142
While we are at it, let’s make a frosting recipe, and bake it.
frosting_recipe <- recipe(sugar + butter + vanilla + milk)
frosting_recipe
## sugar + butter + vanilla + milk
frosting <- bake(frosting_recipe)
## Error in bake(frosting_recipe): object 'butter' not found
We got an error when we tried to ‘bake’ the frosting because we didn’t put any butter in the pantry. But note that writing down the recipe completed successfully! I mean, obviously, right? You don’t need to actually have butter in the pantry in order to write down a recipe that involves butter. But you DO need butter when you want to bake that recipe.
The Recipe versus the Cake Part 2
So what can you do with the expression (recipe) other than evaluate it (bake it)? You can pass it to a function. Imagine your friend really liked the cake and wants the recipe. Giving your friend a cake is very different from giving your friend the recipe for cake.
To make it easier to tell what is going on, our friend just prints out whatever you give him.
friend <- function(gift) {
print(gift)
}
friend(cake)
## [1] 142
friend(cake_recipe)
## flour + sugar + eggs
Here is what it looks like when you give your friend a cake.
friend(cake)
## [1] 142
friend(flour + sugar + eggs)
## [1] 142
friend(bake(cake_recipe))
## [1] 142
All three of these look the same because the expression (recipe) is evaluated (baked) before your friend prints it out.
Here is what it looks like when you give your friend a recipe for a cake.
friend(cake_recipe)
## flour + sugar + eggs
friend(recipe(flour + sugar + eggs))
## flour + sugar + eggs
There is one more way to send your friend a recipe instead of a cake. Showing it requires a new friend that really REALLY demands to see the recipe.
friend2 <- function(recipe){
recipe <- rlang::enexpr(recipe)
print(recipe)
}
friend2(flour + sugar + eggs)
## flour + sugar + eggs
The rlang
package has enquo
, enexpr
, and soon ensym
. These en...
variants all mean something like: go back and make an expression (recipe) out of what was inside the function call. Once more, let’s emphasize how even though the calls look the same, the friend
function ends up with a cake, whereas the friend2
ends up with a recipe.
friend(flour + sugar + eggs)
## [1] 142
friend2(flour + sugar + eggs)
## flour + sugar + eggs
Dplyr functions are functions that work on a combination of regular objects and expressions. The data
argument is a regular object, but most of the other arguments use enexpr
type tricks to go back and get expressions from you. You don’t feel like you are passing in an expression … but you are.
Back to the Meta Recipe
It’s time to get back to the original recipe for Maple Butternut Cake that your friend wanted. Maybe take a look at that photo above again. This is a “meta-recipe” or “higher-level recipe” in that it is a recipe (expression) which refers to other recipes (expressions).
butternut_cake_recipe <- recipe((cake_recipe) + butternuts + (frosting_recipe))
friend(butternut_cake_recipe)
## (cake_recipe) + butternuts + (frosting_recipe)
Clearly this is the unsatisfying and incomplete. We previously wrote down cake_recipe
and frosting_recipe
from basic ingredients. We just need a way to replace the symbol cake_recipe
with the full recipe.
Unquoting is for Re-Writing a Meta-Recipe
This is the core purpose of the unquoting operator in rlang
: !!
or UQ()
. This operator allows us to swap in the sub-recipes (sub-expressions) so that we can re-write a full, complete recipe starting from basic ingredients.
full_butternut_cake_recipe <- recipe((!!cake_recipe) + butternuts + (!!frosting_recipe))
friend(full_butternut_cake_recipe)
## (flour + sugar + eggs) + butternuts + (sugar + butter + vanilla +
## milk)
Let’s swap in the sub-recipes with friend2
as well. In this case, it isn’t necessary to explicitly capture the expression with recipe
because the friend2
function uses enexpr
to capture whatever is inside the function call as a recipe (expression).
friend2((!!cake_recipe) + butternuts + (!!frosting_recipe))
## (flour + sugar + eggs) + butternuts + (sugar + butter + vanilla +
## milk)
Putting the Pieces Together in a Dplyr-Like Function
To put it all together, let’s write a function that works in a dplyr-like way. The frost_it
function will take one regular object (a cake), and one expression (a recipe for frosting). It will then evaluate the expression, add it to the cake, and return the result. It will use enexpr
so that it conceals the expression-nature of the frosting_recipe argument from the user.
frost_it <- function(cake, frosting_recipe) {
frosting_recipe <- rlang::enexpr(frosting_recipe)
#stock the pantry
sugar <- 75
butter <- 60
vanilla <- 5
milk <- 40
frosting <- bake(frosting_recipe)
frosted_cake <- cake + frosting
frosted_cake
}
frost_it(cake, frosting_recipe = sugar + butter + vanilla + milk)
## [1] 322
We did it!
Wrapping Up
This analogy probably doesn’t capture everything, but I think it has helped me understand better what is going on with quasi-quotation, and I hope it will help you. One thing that I have found confusing is the distinction between evaluation and unquoting. Importantly, you only use the unquoting operator !!
inside of quoting functions (like expr
, quo
, or … “recipe”) because it is for re-writing recipes. The end product will be another recipe (expression). By contrast, evaluating will result in a normal value – a cake!
R term | Analogy term | Example |
---|---|---|
Expression | Recipe | expr(flour + sugar + eggs) |
Environment | Pantry | objects() # check objects in the environment |
Evaluation | Baking | eval_tidy(cake_recipe) |
Quasi-Quotation Expression | Meta-Recipe | !!cake_recipe + butternuts + !!frosting_recipe |
Unquoting !! /UQ() |
Re-writing a Recipe | (as above) |