Private Members in R Reference Class
Asked Answered
Z

3

14

Is it possible to have private member fields inside of an R reference class. Playing with some of the online examples I have:

> Account <- setRefClass(    "ref_Account"
>      , fields = list(
>       number = "character"
>       , balance ="numeric")
>      , methods = list( 
>     deposit <- function(amount) {
>       if(amount < 0)   {
>         stop("deposits must be positive")
>       }
>       balance <<- balance + amount
>     }
>     , withdraw <- function(amount) {
>       if(amount < 0)   {
>         stop("withdrawls must be positive")
>       }
>       balance <<- balance - amount
>     }       
>   ) )
> 
> 
> tb <- Account$new(balance=50.75, number="baml-029873") tb$balance
> tb$balance <- 12 
> tb$balance

I hate the fact I can update the balance directly. Perhaps that the old pure OO in me, I really would like to be able make the balance private, at least non-settable from outside the class.

Thoughts

Zambrano answered 16/11, 2011 at 21:57 Comment(1)
The R6-package/framework has private fields and methods build in (and is claimed to be more performant).Synclastic
B
3

This answer doesn't work with R > 3.00, so don't use it!

As has been mentioned, you can't have private member fields. However, if you use the initialize method, then the balance isn't displayed as a field. For example,

Account = setRefClass("ref_Account", 
                       fields = list(number = "character"),
                       methods = list(
                           initialize = function(balance, number) {
                               .self$number = number
                               .self$balance = balance
                           })

As before, we'll create an instance:

tb <- Account$new(balance=50.75, number="baml-0029873")
##No balance
tb

Reference class object of class "ref_Account"
Field "number":
[1] "baml-0029873"

As I mentioned, it isn't truly private, since you can still do:

R> tb$balance
[1] 50.75
R> tb$balance = 12 
R> tb$balance
[1] 12
Bewray answered 8/11, 2012 at 11:9 Comment(1)
Why doesn't this work with R > 3.00? Can you do private members natively now?Fourwheeler
A
5

To solve the issue of privacy I create an own class, "Private", which has new methods to access the object, i.e. $ and [[. These methods will throw an error if the client tries to access 'private' member. Private member are identified by the name (leading period). As reference Objects are environments in R one can work around this, but it is my solution at this time and I think more convenient to use get/set methods provided by the class. So this is more the 'hard-to-set-from-outside-the-class' solution to the question.

I have organised this inside a R-package so the following code makes use of that package and modifies the above example such that an assignment to tb$.balance produces an error. I also use the function Class which is just a wrapper around setRefClass so this is still in the scope of R's reference classes provided by the methods package and used in the question.

devtools::install_github("wahani/aoos")
library("aoos")

Account <- defineRefClass({
    Class <- "Account"
    contains <- "Private"

    number <- "character"
    .balance <- "numeric"

    deposit <- function(amount) {
        if(amount < 0) stop("deposits must be positive")
        .balance <<- .balance + amount
    }

    withdraw <- function(amount) {
        if(amount < 0) stop("withdrawls must be positive")
        .balance <<- .balance - amount
    }
})

tb <- Account(.balance = 50.75, number = "baml-029873") 
tb$.balance # error
tb$.balance <- 12 # error
Awad answered 23/3, 2015 at 15:24 Comment(0)
B
3

This answer doesn't work with R > 3.00, so don't use it!

As has been mentioned, you can't have private member fields. However, if you use the initialize method, then the balance isn't displayed as a field. For example,

Account = setRefClass("ref_Account", 
                       fields = list(number = "character"),
                       methods = list(
                           initialize = function(balance, number) {
                               .self$number = number
                               .self$balance = balance
                           })

As before, we'll create an instance:

tb <- Account$new(balance=50.75, number="baml-0029873")
##No balance
tb

Reference class object of class "ref_Account"
Field "number":
[1] "baml-0029873"

As I mentioned, it isn't truly private, since you can still do:

R> tb$balance
[1] 50.75
R> tb$balance = 12 
R> tb$balance
[1] 12
Bewray answered 8/11, 2012 at 11:9 Comment(1)
Why doesn't this work with R > 3.00? Can you do private members natively now?Fourwheeler
D
1

I came across a similar problem and implemented it this way using Base R. I tend to make things harder on myself by not using third party packages like R6. To solve this problem I access the environment where the object methods are defined and store variables that way.

In this example, I am trying to implement a MinMaxScaler like that found in scikit learn:

## Base reference class
setRefClass(
  "Transformer",
  contains = "VIRTUAL",
  methods = list(
    fit = function(data) stop("Must implement"),
    transform = function(data) stop("Must implement"),
    fit_transform = function(data) {
      fit(data)
      transform(data)
    }
  ))

Concrete implementation of the Transformer API. In the fit method, I access the environment where fit is defined. I then use that environment to store whatever variables I need for intermediate calculations and to update the object in place -- just like sklearn.

MinMaxScaler <-setRefClass(
  "MinMaxScaler",
  contains = "Transformer",
  fields = c(feature_range = "numeric"),
  methods = list(

    fit = function(data) {
      env <- environment(fun = .self$fit)
      rng <- range(data, na.rm=TRUE)
      env$data_range_ <- diff(range(data, na.rm=TRUE))
      env$data_min_ <- rng[[1]]
      env$data_max_ <- rng[[2]]
    },
    transform = function(data) {
      env <- environment(fun = .self$transform)
      scalef <- diff(range(feature_range))
      scalef * (data - env$data_min_) / env$data_range_  + min(feature_range)
    }
  )
)

To demonstrate this pattern, I will create two scalers and fit them separately:

> ## Dummy data
> set.seed(123)
> z <- rnorm(1e4)
> summary(z)
     Min.   1st Qu.    Median      Mean   3rd Qu.      Max. 
-3.845320 -0.667969 -0.011089 -0.002372  0.673347  3.847768 
> 
> scaler1 <- MinMaxScaler(feature_range=c(0, 50))
> summary(scaler1$fit_transform(z))
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
   0.00   20.65   24.92   24.98   29.37   50.00 
> 
> scaler2 <- MinMaxScaler(feature_range=c(-100, 100))
> summary(scaler2$fit_transform(z))
      Min.    1st Qu.     Median       Mean    3rd Qu.       Max. 
-100.00000  -17.39725   -0.32011   -0.09347   17.47344  100.00000 
> 
> ## to show the scalers are distinct and not sharing private vars
> summary(scaler1$transform(z))
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
   0.00   20.65   24.92   24.98   29.37   50.00 
> summary(scaler2$transform(z))
      Min.    1st Qu.     Median       Mean    3rd Qu.       Max. 
-100.00000  -17.39725   -0.32011   -0.09347   17.47344  100.00000 
Diplomate answered 20/10, 2018 at 14:0 Comment(1)
Thank you! Using the environment() works great and does not need any particular checks.Inning

© 2022 - 2024 — McMap. All rights reserved.