bookclub-advr

DSLC Advanced R Book Club
git clone https://git.eamoncaddigan.net/bookclub-advr.git
Log | Files | Refs | README | LICENSE

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 ```