-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathEnvironments.Rmd
428 lines (313 loc) · 10.9 KB
/
Environments.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
---
output:
pdf_document: default
html_document: default
---
# Environments
<!-- ```{r, include = FALSE, eval=FALSE} -->
<!-- # don't include because otherwise all loaded packages will -->
<!-- # show up in search path for environments -->
<!-- source("common.R") -->
<!-- ``` -->
Loading the needed libraries:
```{r Environments-1, warning=FALSE, message=FALSE}
library(rlang, warn.conflicts = FALSE)
```
## Environment basics (Exercises 7.2.7)
**Q1.** List three ways in which an environment differs from a list.
**A1.** As mentioned in the book, here are a few ways in which environments differ from lists:
| Property | List | Environment |
| :-----------------: | :-----: | :---------: |
| semantics | value | reference |
| data structure | linear | non-linear |
| duplicated names | allowed | not allowed |
| can have parents? | false | true |
| can contain itself? | false | true |
**Q2.** Create an environment as illustrated by this picture.
```{r Environments-2, echo = FALSE}
knitr::include_graphics("diagrams/environments/recursive-1.png")
```
**A2.** Creating the environment illustrated in the picture:
```{r Environments-3}
library(rlang)
e <- env()
e$loop <- e
env_print(e)
```
The binding `loop` should have the same memory address as the environment `e`:
```{r Environments-4}
lobstr::ref(e$loop)
```
**Q3.** Create a pair of environments as illustrated by this picture.
```{r Environments-5, echo = FALSE}
knitr::include_graphics("diagrams/environments/recursive-2.png")
```
**A3.** Creating the specified environment:
```{r Environments-6}
e1 <- env()
e2 <- env()
e1$loop <- e2
e2$deloop <- e1
# following should be the same
lobstr::obj_addrs(list(e1, e2$deloop))
lobstr::obj_addrs(list(e2, e1$loop))
```
**Q4.** Explain why `e[[1]]` and `e[c("a", "b")]` don't make sense when `e` is an environment.
**A4.** An environment is a non-linear data structure, and has no concept of ordered elements. Therefore, indexing it (e.g. `e[[1]]`) doesn't make sense.
Subsetting a list or a vector returns a subset of the underlying data structure. For example, subsetting a vector returns another vector. But it's unclear what subsetting an environment (e.g. `e[c("a", "b")]`) should return because there is no data structure to contain its returns. It can't be another environment since environments have reference semantics.
**Q5.** Create a version of `env_poke()` that will only bind new names, never re-bind old names. Some programming languages only do this, and are known as [single assignment languages](https://en.wikipedia.org/wiki/Assignment_(computer_science)#Single_assignment).
**A5.** Create a version of `env_poke()` that doesn't allow re-binding old names:
```{r Environments-7}
env_poke2 <- function(env, nm, value) {
if (env_has(env, nm)) {
abort("Can't re-bind existing names.")
}
env_poke(env, nm, value)
}
```
Making sure that it behaves as expected:
```{r Environments-8, error=TRUE}
e <- env(a = 1, b = 2, c = 3)
# re-binding old names not allowed
env_poke2(e, "b", 4)
# binding new names allowed
env_poke2(e, "d", 8)
e$d
```
Contrast this behavior with the following:
```{r Environments-9}
e <- env(a = 1, b = 2, c = 3)
e$b
# re-binding old names allowed
env_poke(e, "b", 4)
e$b
```
**Q6.** What does this function do? How does it differ from `<<-` and why might you prefer it?
```{r Environments-10, error = TRUE}
rebind <- function(name, value, env = caller_env()) {
if (identical(env, empty_env())) {
stop("Can't find `", name, "`", call. = FALSE)
} else if (env_has(env, name)) {
env_poke(env, name, value)
} else {
rebind(name, value, env_parent(env))
}
}
rebind("a", 10)
a <- 5
rebind("a", 10)
a
```
**A6.** The downside of `<<-` is that it will create a new binding if it doesn't exist in the given environment, which is something that we may not wish:
```{r Environments-11}
# `x` doesn't exist
exists("x")
# so `<<-` will create one for us
{
x <<- 5
}
# in the global environment
env_has(global_env(), "x")
x
```
But `rebind()` function will let us know if the binding doesn't exist, which is much safer:
```{r Environments-12, error=TRUE}
rebind <- function(name, value, env = caller_env()) {
if (identical(env, empty_env())) {
stop("Can't find `", name, "`", call. = FALSE)
} else if (env_has(env, name)) {
env_poke(env, name, value)
} else {
rebind(name, value, env_parent(env))
}
}
# doesn't exist
exists("abc")
# so function will produce an error instead of creating it for us
rebind("abc", 10)
# but it will work as expected when the variable already exists
abc <- 5
rebind("abc", 10)
abc
```
## Recursing over environments (Exercises 7.3.1)
**Q1.** Modify `where()` to return _all_ environments that contain a binding for `name`. Carefully think through what type of object the function will need to return.
**A1.** Here is a modified version of `where()` that returns _all_ environments that contain a binding for `name`.
Since we anticipate more than one environment, we dynamically update a list each time an environment with the specified binding is found. It is important to initialize to an empty list since that signifies that given binding is not found in any of the environments.
```{r Environments-13}
where <- function(name, env = caller_env()) {
env_list <- list()
while (!identical(env, empty_env())) {
if (env_has(env, name)) {
env_list <- append(env_list, env)
}
env <- env_parent(env)
}
return(env_list)
}
```
Let's try it out:
```{r Environments-14}
where("yyy")
x <- 5
where("x")
where("mean")
library(dplyr, warn.conflicts = FALSE)
where("filter")
detach("package:dplyr")
```
**Q2.** Write a function called `fget()` that finds only function objects. It should have two arguments, `name` and `env`, and should obey the regular scoping rules for functions: if there's an object with a matching name that's not a function, look in the parent. For an added challenge, also add an `inherits` argument which controls whether the function recurses up the parents or only looks in one environment.
**A2.** Here is a function that recursively looks for function objects:
```{r Environments-15}
fget <- function(name, env = caller_env(), inherits = FALSE) {
# we need only function objects
f_value <- mget(name,
envir = env,
mode = "function",
inherits = FALSE, # since we have our custom argument
ifnotfound = list(NULL)
)
if (!is.null(f_value[[1]])) {
# success case
f_value[[1]]
} else {
if (inherits && !identical(env, empty_env())) {
# recursive case
env <- env_parent(env)
fget(name, env, inherits = TRUE)
} else {
# base case
stop("No function objects with matching name was found.", call. = FALSE)
}
}
}
```
Let's try it out:
```{r Environments-16, error=TRUE}
fget("mean", inherits = FALSE)
fget("mean", inherits = TRUE)
mean <- 5
fget("mean", inherits = FALSE)
mean <- function() NULL
fget("mean", inherits = FALSE)
rm("mean")
```
## Special environments (Exercises 7.4.5)
**Q1.** How is `search_envs()` different from `env_parents(global_env())`?
**A1.** The `search_envs()` lists a chain of environments currently attached to the search path and contains exported functions from these packages. The search path always ends at the `{base}` package environment. The search path also includes the global environment.
```{r Environments-17}
search_envs()
```
The `env_parents()` lists all parent environments up until the empty environment. Of course, the global environment itself is not included in this list.
```{r Environments-18}
env_parents(global_env())
```
**Q2.** Draw a diagram that shows the enclosing environments of this function:
```{r Environments-19, eval = FALSE}
f1 <- function(x1) {
f2 <- function(x2) {
f3 <- function(x3) {
x1 + x2 + x3
}
f3(3)
}
f2(2)
}
f1(1)
```
**A2.** I don't have access to the graphics software used to create diagrams in the book, so I am linking the diagram from the [official solutions manual](https://advanced-r-solutions.rbind.io/environments.html#special-environments), where you will also find a more detailed description for the figure:
```{r Environments-20, echo=FALSE, out.width = '100%', eval=!knitr::is_latex_output()}
knitr::include_graphics("https://raw.githubusercontent.com/Tazinho/Advanced-R-Solutions/main/images/environments/function_environments_corrected.png", auto_pdf = TRUE)
```
**Q3.** Write an enhanced version of `str()` that provides more information about functions. Show where the function was found and what environment it was defined in.
**A3.** To write the required function, we can first re-purpose the `fget()` function we wrote above to return the environment in which it was found and its enclosing environment:
```{r Environments-21}
fget2 <- function(name, env = caller_env()) {
# we need only function objects
f_value <- mget(name,
envir = env,
mode = "function",
inherits = FALSE,
ifnotfound = list(NULL)
)
if (!is.null(f_value[[1]])) {
# success case
list(
"where" = env,
"enclosing" = fn_env(f_value[[1]])
)
} else {
if (!identical(env, empty_env())) {
# recursive case
env <- env_parent(env)
fget2(name, env)
} else {
# base case
stop("No function objects with matching name was found.", call. = FALSE)
}
}
}
```
Let's try it out:
```{r Environments-22}
fget2("mean")
mean <- function() NULL
fget2("mean")
rm("mean")
```
We can now write the new version of `str()` as a wrapper around this function. We only need to foresee that the users might enter the function name either as a symbol or a string.
```{r Environments-23}
str_function <- function(.f) {
fget2(as_string(ensym(.f)))
}
```
Let's first try it with `base::mean()`:
```{r Environments-24}
str_function(mean)
str_function("mean")
```
And then with our variant present in the global environment:
```{r Environments-25}
mean <- function() NULL
str_function(mean)
str_function("mean")
rm("mean")
```
## Call stacks (Exercises 7.5.5)
**Q1.** Write a function that lists all the variables defined in the environment in which it was called. It should return the same results as `ls()`.
**A1.** Here is a function that lists all the variables defined in the environment in which it was called:
```{r Environments-26}
# let's first remove everything that exists in the global environment right now
# to test with only newly defined objects
rm(list = ls())
rm(.Random.seed, envir = globalenv())
ls_env <- function(env = rlang::caller_env()) {
sort(rlang::env_names(env))
}
```
The workhorse here is `rlang::caller_env()`, so let's also have a look at its definition:
```{r Environments-27}
rlang::caller_env
```
Let's try it out:
- In global environment:
```{r Environments-28}
x <- "a"
y <- 1
ls_env()
ls()
```
- In function environment:
```{r Environments-29}
foo <- function() {
a <- "x"
b <- 2
print(ls_env())
print(ls())
}
foo()
```
## Session information
```{r Environments-30}
sessioninfo::session_info(include_base = TRUE)
```