17.Rmd (4829B)
1 --- 2 engine: knitr 3 title: Big picture 4 --- 5 6 ## Learning objectives: 7 8 - Become familiar with some metaprogramming principals and how they relate to each other 9 - Review vocabulary associated with metaprogramming 10 11 ```{r} 12 library(rlang) 13 library(lobstr) 14 ``` 15 16 17 ## Code is data 18 19 - **expression** - Captured code (*call*, *symbol*, *constant*, or *pairlist*) 20 - Use `rlang::expr()`[^1] to capture code directly 21 22 ```{r} 23 expr(mean(x, na.rm = TRUE)) 24 ``` 25 26 - Use `rlang::enexpr()` to capture code indirectly 27 28 ```{r} 29 capture_it <- function(x) { # 'automatically quotes first argument' 30 enexpr(x) 31 } 32 capture_it(a + b + c) 33 ``` 34 35 - 'Captured' code can be modified (like a list)! 36 - First element is the function, next elements are the arguments 37 38 ```{r} 39 f <- expr(f(x = 1, y = 2)) 40 names(f) 41 42 ff <- fff <- f # Create two copies 43 44 ff$z <- 3 # Add an argument to one 45 fff[[2]] <- NULL # Remove an argument from another 46 47 f 48 ff 49 fff 50 ``` 51 52 > More on this next week! 53 54 [^1]: Equivalent to `base::bquote()` 55 56 ## Code is a tree 57 58 - **Abstract syntax tree** (AST) - Almost every language represents code as a tree 59 - Use `lobstr::ast()` to inspect these code trees 60 61 ```{r} 62 ast(f1(f2(a, b), f3(1))) 63 ast(1 + 2 * 3) 64 ``` 65 66 67 ## Code can generate code 68 69 - `rlang::call2()` creates function call 70 71 ```{r} 72 call2("f", 1, 2, 3) 73 ``` 74 75 - Going backwards from the tree, can use functions to create calls 76 77 ```{r} 78 call2("f1", call2("f2", "a", "b"), call2("f3", 1)) 79 call2("+", 1, call2("*", 2, 3)) 80 ``` 81 82 - `!!` bang-bang - **unquote operator** 83 - inserts previously defined expressions into the current one 84 85 ```{r} 86 xx <- expr(x + x) 87 yy <- expr(y + y) 88 expr(xx / yy) # Nope! 89 90 expr(!!xx / !!yy) # Yup! 91 ``` 92 93 ```{r} 94 cv <- function(var) { 95 var <- enexpr(var) # Get user's expression 96 expr(sd(!!var) / mean(!!var)) # Insert user's expression 97 } 98 99 cv(x) 100 cv(x + y) 101 ``` 102 103 - Avoid `paste()` for building code 104 - Problems with non-syntactic names and precedence among expressions 105 106 > "You might think this is an esoteric concern, but not worrying about it when generating SQL code in web applications led to SQL injection attacks that have collectively cost billions of dollars." 107 108 ## Evaluation runs code 109 110 - **evaluate** - run/execute an expression 111 - need both expression and environment 112 - `eval()` uses current environment if not set 113 - manual evaluation means you can tweak the environment! 114 115 ```{r} 116 xy <- expr(x + y) 117 118 eval(xy, env(x = 1, y = 10)) 119 eval(xy, env(x = 2, y = 100)) 120 ``` 121 122 123 ## Customizing evaluations with functions 124 - Can also bind names to functions in supplied environment 125 - Allows overriding function behaviour 126 - This is how dplyr generates SQL for working with databases 127 128 For example... 129 ```{r} 130 string_math <- function(x) { 131 e <- env( 132 caller_env(), 133 `+` = function(x, y) paste(x, y), 134 `*` = function(x, y) strrep(x, y) 135 ) 136 137 eval(enexpr(x), e) 138 } 139 140 cohort <- 9 141 string_math("Hello" + "cohort" + cohort) 142 string_math(("dslc" + "is" + "awesome---") * cohort) 143 ``` 144 145 146 ## Customizing evaluation with data 147 148 - Look for variables inside data frame 149 - **Data mask** - typically a data frame 150 - use `rlang::eval_tidy()` rather than `eval()` 151 152 ```{r} 153 df <- data.frame(x = 1:5, y = sample(5)) 154 eval_tidy(expr(x + y), df) 155 ``` 156 157 Catch user input with `enexpr()`... 158 159 ```{r} 160 with2 <- function(df, expr) { 161 eval_tidy(enexpr(expr), df) 162 } 163 164 with2(df, x + y) 165 ``` 166 167 But there's a bug! 168 169 - Evaluates in environment inside `with2()`, but the expression likely refers 170 to objects in the Global environment 171 172 ```{r} 173 with2 <- function(df, expr) { 174 a <- 1000 175 eval_tidy(enexpr(expr), df) 176 } 177 178 df <- data.frame(x = 1:3) 179 a <- 10 180 with2(df, x + a) 181 ``` 182 183 - Solved with Quosures... 184 185 ## Quosures 186 187 - **Quosures** bundles expression with an environment 188 - Use `enquo()` instead of `enexpr()` (with `eval_tidy()`) 189 190 ```{r} 191 with2 <- function(df, expr) { 192 a <- 1000 193 eval_tidy(enquo(expr), df) 194 } 195 196 df <- data.frame(x = 1:3) 197 a <- 10 198 with2(df, x + a) 199 ``` 200 201 > "Whenever you use a data mask, you must always use `enquo()` instead of `enexpr()`. 202 203 This comes back in Chapter 20. 204 205 ### Which environment is bundled? 206 - The environment where the expression is created (i.e. the parent of where 207 `enquo()` is called) 208 209 Here, the global environment 210 211 ```{r} 212 with2 <- function(df, expr) { 213 a <- 1000 214 eq <- enquo(expr) 215 message("with2() Parent/Calling environment: ") 216 print(rlang::caller_env()) 217 message("with2() environment: ") 218 print(rlang::current_env()) 219 message("Quosure details: ") 220 print(eq) # Print the details of the quosure 221 eval_tidy(eq, df) 222 } 223 224 a <- 10000 225 df <- data.frame(x = 1:3) 226 with2(df, x + a) 227 ``` 228 229 230 Here, the `fun1()` environment 231 ```{r} 232 fun1 <- function(df) { 233 a <- 10 234 message("fun1() Parent/Calling environment: ") 235 print(rlang::caller_env()) 236 message("fun1() environment: ") 237 print(rlang::current_env()) 238 with2(df, x + a) 239 } 240 241 a <- 10000 242 df <- data.frame(x = 1:3) 243 fun1(df) 244 ```