Closures are often a poorman alternative to OOP (object oriental programming), not only because they could be easier implemented but because they are fast. However, it is quite easy to get things wrong if one is not careful enough. Tools are provided to create and manipulate closures.
You can install the released version of closure from CRAN with:
install.packages("closure") # not yet released
And the development version from GitHub with:
# install.packages("devtools")
devtools::install_github("randy3k/closure")
library(closure)
Foo1 <- new_closure(list(
counter = NULL,
initialize = function(x) {
counter <<- x
},
add = function(x) {
counter <<- counter + x
invisible(self)
}
))
Foo1
is actually just a function with the corresponding members.
Foo1
#> function (x)
#> {
#> self <- environment()
#> force(x)
#> on.exit({
#> rm(x)
#> })
#> counter <- NULL
#> initialize <- function(x) {
#> counter <<- x
#> }
#> add <- function(x) {
#> counter <<- counter + x
#> invisible(self)
#> }
#> initialize(x = x)
#> self
#> }
foo1 <- Foo1(1)
foo1$add(3)$counter
#> [1] 4
Foo2 <- toR6("Foo2", Foo1)
foo2 <- Foo2$new(1)
foo2$add(3)$counter
#> [1] 4
Foo3 <- toR6("Foo3", Foo1, portable = FALSE)
foo3 <- Foo3$new(1)
# method chaining does not work for non portable class
foo3$add(3)
foo3$counter
#> [1] 4
Why closure
when there is R6
? It’s because closures are much faster. Even faster than R6
when portable
is TRUE
.
bench::mark(
closure = foo1$add(3),
R6 = foo2$add(3),
`R6 Portable` = foo3$add(3),
check = FALSE
)
#> # A tibble: 3 x 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
#> 1 closure 657ns 848ns 523531. 0B 0
#> 2 R6 3.88µs 4.51µs 157162. 0B 15.7
#> 3 R6 Portable 1.62µs 1.93µs 371429. 0B 0
Then why you need this package to write closure? Because it is incredibly easier to make things wrong. See the following example inspired by advanced-R.
g <- function(k) {
function(x) {
x^k
}
}
m <- 2
h1 <- g(2)
h2 <- g(m)
m <- 3
identical(h1(2), h2(2))
#> [1] FALSE