/
Portable.Rmd
211 lines (159 loc) · 5.27 KB
/
Portable.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
---
title: "Portable and non-portable R6 classes"
output:
html_document:
theme: null
css: mystyle.css
toc: yes
---
<!--
%\VignetteEngine{knitr::rmarkdown}
%\VignetteIndexEntry{Portable R6 classes}
-->
```{r echo = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
```
One limitation to R's reference classes is that class inheritance across package namespaces is limited. R6 avoids this problem when the `portable` option is enabled.
## The problem
Here is an example of the cross-package inheritance problem with reference classes: Suppose you have ClassA in pkgA, and ClassB in pkgB, which inherits from ClassA. ClassA has a method `foo` which calls a non-exported function `fun` in pkgA.
If ClassB inherits `foo`, it will try to call `fun` -- but since ClassB objects are created in pkgB namespace (which is an environment) instead of the pkgA namespace, it won't be able to find `fun`.
Something similar happens with R6 when the `portable=FALSE` option is used. For example:
```{r}
library(R6)
# Simulate packages by creating environments
pkgA <- new.env()
pkgB <- new.env()
# Create a function in pkgA but not pkgB
pkgA$fun <- function() 10
ClassA <- R6Class("ClassA",
portable = FALSE,
public = list(
foo = function() fun()
),
parent_env = pkgA
)
# ClassB inherits from ClassA
ClassB <- R6Class("ClassB",
portable = FALSE,
inherit = ClassA,
parent_env = pkgB
)
```
When we create an instance of ClassA, it works as expected:
```{r}
a <- ClassA$new()
a$foo()
```
But with ClassB, it can't find the `foo` function:
```{r eval=FALSE}
b <- ClassB$new()
b$foo()
#> Error in b$foo() : could not find function "fun"
```
## Portable R6
R6 supports inheritance across different packages, with the default `portable=TRUE` option. In this example, we'll again simulate different packages by creating separate parent environments for the classes.
```{r}
pkgA <- new.env()
pkgB <- new.env()
pkgA$fun <- function() {
"This function `fun` in pkgA"
}
ClassA <- R6Class("ClassA",
portable = TRUE, # The default
public = list(
foo = function() fun()
),
parent_env = pkgA
)
ClassB <- R6Class("ClassB",
portable = TRUE,
inherit = ClassA,
parent_env = pkgB
)
a <- ClassA$new()
a$foo()
b <- ClassB$new()
b$foo()
```
When a method is inherited from a superclass, that method also gets that class's environment. In other words, method "runs in" the superclass's environment. This makes it possible for inheritance to work across packages.
When a method is defined in the subclass, that method gets the subclass's environment. For example, here ClassC is a subclass of ClassA, and defines its own `foo` method which overrides the `foo` method from ClassA. It happens that the method looks the same as ClassA's -- it just calls `fun`. But this time it finds `pkgC$fun` instead of `pkgA$fun`. This is in contrast to ClassB, which inherited the `foo` method and environment from ClassA.
```{r}
pkgC <- new.env()
pkgC$fun <- function() {
"This function `fun` in pkgC"
}
ClassC <- R6Class("ClassC",
portable = TRUE,
inherit = ClassA,
public = list(
foo = function() fun()
),
parent_env = pkgC
)
cc <- ClassC$new()
# This method is defined in ClassC, so finds pkgC$fun
cc$foo()
```
## Using `self`
One important difference between non-portable and portable classes is that with non-portable classes, it's possible to access members with just the name of the member, and with portable classes, member access always requires using `self$` or `private$`. This is a consequence of the inheritance implementation.
Here's an example of a non-portable class with two methods: `sety`, which sets the private field `y` using the `<<-` operator, and `getxy`, which returns a vector with the values of fields `x` and `y`:
```{r}
NP <- R6Class("NP",
portable = FALSE,
public = list(
x = 1,
getxy = function() c(x, y),
sety = function(value) y <<- value
),
private = list(
y = NA
)
)
np <- NP$new()
np$sety(20)
np$getxy()
```
If we attempt the same with a portable class, it results in an error:
```{r eval=FALSE}
P <- R6Class("P",
portable = TRUE,
public = list(
x = 1,
getxy = function() c(x, y),
sety = function(value) y <<- value
),
private = list(
y = NA
)
)
p <- P$new()
# No error, but instead of setting private$y, this sets y in the global
# environment! This is because of the sematics of <<-.
p$sety(20)
y
#> [1] 20
p$getxy()
#> Error in p$getxy() : object 'y' not found
```
To make this work with a portable class, we need to use `self$x` and `private$y`:
```{r}
P2 <- R6Class("P2",
portable = TRUE,
public = list(
x = 1,
getxy = function() c(self$x, private$y),
sety = function(value) private$y <- value
),
private = list(
y = NA
)
)
p2 <- P2$new()
p2$sety(20)
p2$getxy()
```
There is a small performance penalty for using `self$x` as opposed to `x`. In most cases, this is negligible, but it can be noticeable in some situations where there are tens of thousands or more accesses per second. For more information, see the Performance vignette.
## Wrap-up
In summary:
* Portable classes allow inheritance across different packages.
* Portable classes always require the use of `self` or `private` to access members. This can incur a small performance penalty, since using `self$x` is slower than just `x`.