Motivation
I am developing an R package (call it pkg
) which collects in a running cache some objects generated by its function calls. It is simple enough to implement the cache as a list
object (call it .cache
) in the pkg
namespace. However, I want to prevent the user from interfering directly with the cache, which can always be exposed via the :::
syntax: pkg:::.cache
.
That is, I want to protect .cache
analogously to private fields in object-oriented programming. All operations on .cache
should be done by internal helper functions, which are called exclusively by @export
ed functions with an "API flavor".
The Problem
As such, I had the idea of defining an environment
object (call it .vault
) in the namespace, so I can...put my .cache
in my .vault
.
My quandary is that an environment like .vault
is one of the few things in R that work by reference. So I fear pkg:::.vault
will expose .vault
or its contents to modification as well as to viewing. I can alter my accessor functions like pkg:::.get_cache()
so they don't enable such exposure, but all such efforts are in vain if .vault
can be directly modified in place.
DO NOT RUN
By way of example, consider this dangerous (?) code below. First I examine the internal .S3MethodsClasses
environment from dplyr
, to "identify a target":
dplyr:::.S3MethodsClasses
#> <environment: 0x7fab9c6c5310>
ls(dplyr:::.S3MethodsClasses)
#> [1] "grouped_df" "rowwise_df"
Then I examine the rowwise_df
object and deem it a worthy "target":
dplyr:::.S3MethodsClasses$rowwise_df
#> Virtual Class "rowwise_df" [package "dplyr"]
#>
#> Slots:
#>
#> Name: .Data names row.names
#> Class: list character data.frameRowLabels
#>
#> Name: .S3Class
#> Class: character
#>
#> Extends:
#> Class "tbl_df", directly
#> Class "tbl", by class "tbl_df", distance 2
# ...
Finally, I exploit the fact that unlike most objects from the dplyr
namespace...
library(dplyr)
mutate <- NULL
dplyr::mutate
#> function (.data, ...)
#> {
#> UseMethod("mutate")
#> }
#> <bytecode: 0x7fab9c2f6120>
#> <environment: namespace:dplyr>
...an environment like .S3MethodsClasses
points by reference, and so its contents can be easily modified in place:
# "Copy" the pointer to allow `<-` assignment.
same_pointer <- dplyr:::.S3MethodsClasses
same_pointer
#> <environment: 0x7fab9c6c5310>
# Modify in place.
same_pointer$rowwise_df <- NULL
dplyr:::.S3MethodsClasses$rowwise_df
#> NULL
And just like that, the "vault" is robbed!
Suspicions
I suspect the answer might lie here, with lockEnvironment()
and friends, but the application is a bit beyond me. Perhaps I could do something .onLoad
to set up the .vault
environment, which would then be lock*()
ed — both the .vault
itself and the bindings within it — but not before making an active binding between .cache
and an accessor function .get_cache()
, which would populate within the pkg
namespace.
Note
I am already developing a feature to terminate (helper) functions like .get_cache()
when they are called outside a fellow function from the pkg
namespace. Thus, the exposure of pkg:::.get_cache()
will not let the user operate .get_cache()
either manually or in a custom function of their own.
Canonicity
I would especially appreciate the advice of R developers experienced enough to provide a canonical answer (if any).