This question already has an answer here:
`print` method for ReferenceClass
(1 answer)
Closed 8 years ago.
I have a ReferenceClass in R.
How can I add a method "print()" to it, which will print the values of all of the fields in the class?
Perhaps a better implementation is the following
Config = setRefClass("Config",
fields = list(
ConfigBool = "logical",
ConfigString = "character"),
methods = list(
## Allow ... and callSuper for proper initialization by subclasses
initialize = function(...) {
callSuper(..., ConfigBool=TRUE, ConfigString="A configuration string")
## alterantive:
## callSuper(...)
## initFields(ConfigBool=TRUE, ConfigString="A configuration string")
},
## Implement 'show' method for automatic display
show = function() {
flds <- getRefClass()$fields()
cat("* Fields\n")
for (fld in names(flds)) # iterate over flds, rather than index of flds
cat(' ', fld,': ', .self[[fld]], '\n', sep="")
})
)
The following illustrates use of the Config constructor (no need to invoke 'new') and automatic invocation of 'show'
> Config()
* Fields
ConfigBool: TRUE
ConfigString: A configuration string
Run the following demo in an R console:
# Reference Class to store configuration
Config <- setRefClass("Config",
fields = list(
ConfigBool = "logical",
ConfigString = "character"
),
methods = list(
# Constructor.
initialize = function(x) {
ConfigBool <<- TRUE
ConfigString <<- "A configuration string"
},
# Print the values of all of the fields used in this class.
print = function(values) {
cat("* Fields\n")
fieldList <- names(.refClassDef#fieldClasses)
for(fi in fieldList)
{
variableName = fi
variableValue = field(fi)
cat(' ',variableName,': ',variableValue,'\n',sep="")
}
}
)
)
config <- Config$new()
config
config$print()
---test code---
# Demos how to print the fields of the class using built-in default "show()" function.
> config$show()
Reference class object of class "Config"
Field "ConfigBool":
[1] TRUE
Field "ConfigString":
[1] "A configuration string"
# Omitting the "show()" function has the same result, as show() is called by default.
> config
Reference class object of class "Config"
Field "ConfigBool":
[1] TRUE
Field "ConfigString":
[1] "A configuration string"
# Demos how to print the fields of the class using our own custom "print" function.
> config$print()
* Fields
ConfigBool: TRUE
ConfigString: A configuration string
In addition, typing the following shows the source code for the default "show" function that is included with all ReferenceClasses:
config$show
Related
I'm looking at the code example provided in the R6 class documentation here.
This is the class definition:
Person <- R6Class("Person",
private = list(
.age = NA,
.name = NULL
),
active = list(
age = function(value) {
if (missing(value)) {
private$.age
} else {
stop("`$age` is read only", call. = FALSE)
}
},
name = function(value) {
if (missing(value)) {
private$.name
} else {
stopifnot(is.character(value), length(value) == 1)
private$.name <- value
self
}
}
),
public = list(
initialize = function(name, age = NA) {
private$.name <- name
private$.age <- age
}
)
)
The important part is how the age field is wrapped to be read-only, and the name field is wrapped to perform a validation check before making the assignment.
In my own usecase, I'm not interested in the read-only part, but I am implementing the validation logic. So, focus on the active field for name.
I do not understand why they are following the pattern to return self after the call. How can you chain together assignments? Assuming age wasn't read only, how would it look to try to assign age and name in a chain? I've tried a number of ways, and it never seems to work:
p <- Person$new();
p$age <- 10$name <- "Jill" # obviously doesn't work because how can you reference the name field of 10?
(p$age <- 10)$name <- "Jill" # this looks more likely to work but the parens don't help
p$age(10)$name("Jill") # Does not work, you can't invoke active fields as if they were functions.
# other syntax options?
So, the heart of my question is: if you're using active fields in R6 classes to facilitate some type-checking prior to assignment, you can't really chain those operations together, so why does the official documentation show returning self in the field accessors?
Problem
I am working on project with RC various custom made classes. I would like to save certain class instances at the end of my program in order to export them to a data base. I tried using jsonlite::toJSON(account) but I received the error message
Error: No method for S4 class:BankAccount
Class
I have the following class
BankAccount <- setRefClass('BankAccount',
fields = list(
balance = 'numeric',
ledger = 'data.frame'
),
methods = list(
deposit = function (x) {
x <- max(x,0)
balance <<- balance + x
ledger <<- data.frame(
Date = c(ledger$Date, as.character(Sys.time())),
Type = c(ledger$Type, 'Deposit'),
Amount = c(ledger$Amount, x),
stringsAsFactors = FALSE
)
},
withdraw = function (x) {
x <- max(x,0)
balance <<- balance - x
ledger <<- data.frame(
Date = c(ledger$Date, as.character(Sys.time())),
Type = c(ledger$Type, 'Withdrawal'),
Amount = c(ledger$Amount, x),
stringsAsFactors = FALSE
)
}
))
Instance
And here is an instance of that class
account <- BankAccount$new(balance = 100)
account$deposit(1000)
Sys.sleep(5)
account$withdraw(97.89)
account
Reference class object of class "BankAccount"
Field "balance":
[1] 1002.11
Field "ledger":
Date Type Amount
1 2018-12-31 16:21:20 Deposit 1000.00
2 2018-12-31 16:21:26 Withdrawal 97.89
JSON
Now I would like to save it to as a JSON file of the form (there might be a typo in the JSON - not that familiar with the format)
{
"balance": "double",
"ledger": {
"Date": "string",
"Type": "string",
"Amount": "double"
}
}
PS
I also tried without the field ledger (which is of class data.frame) but it still did not work.
Edit
Here is the output of jsonlite::serializeJSON(account)
{"type":"S4","attributes":{".xData":{"type":"environment","attributes":{},"value":{}}},"value":{"class":"BankAccount","package":".GlobalEnv"}}
As you can see it seems to only save information about the class BankAccount but not about the instance account (balance value etc. missing).
Here is a workaround (as indicated by #StefanF). One can add methods to classes
# converts to JSON
toJSON = function (prettifyy = TRUE) {
instanceJSON <- jsonlite::toJSON(list(balance = balance,ledger = ledger))
if (prettifyy) instanceJSON <- jsonlite::prettify(instanceJSON)
instanceJSON
}
# saves as .json, e.g. path = 'C:/Test/instance.json'
saveJSON = function (path) {
instanceJSON <- toJSON()
writeLines(instanceJSON, path)
}
The solution is not ideal as
there is a bit of labor as one needs to specify which of the fields should be incorporated
also, if a field of BankAccount is of class MyClass (another custom class), then you need to either specify which fields of MyClass are of relevance or create a toJSON as well for MyClass
I'm using active bindings in an R6 class to check values before assignment to fields. I thought I could use a closure to generate the bindings as below, but this doesn't work.
The binding isn't evaluated in the way I expect (at all?) because the error shows the closure's name argument. What am I missing?
library(R6)
library(pryr)
# pass a field name to create its binding
generate_binding <- function(name) {
function(value) {
if (!missing(value) && length(value) > 0) {
private$name <- value
}
private$name
}
}
bind_x = generate_binding(x_)
# created as intended:
unenclose(bind_x)
# function (value)
# {
# if (!missing(value) && length(value) > 0) {
# private$x_ <- value
# }
# private$x_
# }
MyClass <- R6::R6Class("MyClass",
private = list(
x_ = NULL
),
active = list(
x = bind_x
),
)
my_class_instance <- MyClass$new()
my_class_instance$x <- "foo"
# Error in private$name <- value :
# cannot add bindings to a locked environment
I think you’re misunderstanding how closures work. unenclose is a red herring here (as it doesn’t actually show you what the closure looks like). The closure contains the statement private$name <- value — it does not contain the statement private$x_ <- value.
The usual solution to this problem would be to rewrite the closure such that the unevaluated name argument is deparsed into its string representation, and then used to subset the private environment (private[[name]] <- value). However, this doesn’t work here since R6 active bindings strip closures of their enclosing environment.
This is where unenclose comes in then:
MyClass <- R6::R6Class("MyClass",
private = list(
x_ = NULL
),
active = list(
x = pryr::unenclose(bind_x)
),
)
S4 classes allow you to define validity checks using validObject() or setValidity(). However, this does not appear to work for ReferenceClasses.
I have tried adding assert_that() or if (badness) stop(message) clauses to the $initialize() method of a ReferenceClass. However, when I simulate loading the package (using devtools::load_all()), it must try to create some prototype class because the initialize method executes and fails (because no fields have been set).
What am I doing wrong?
Implement a validity method on the reference class
A = setRefClass("A", fields=list(x="numeric", y="numeric"))
setValidity("A", function(object) {
if (length(object$x) != length(object$y)) {
"x, y lengths differ"
} else NULL
})
and invoke the validity method explicitly
> validObject(A())
[1] TRUE
> validObject(A(x=1:5, y=5:1))
[1] TRUE
> validObject(A(x=1:5, y=5:4))
Error in validObject(A(x = 1:5, y = 5:4)) :
invalid class "A" object: x, y lengths differ
Unfortunately, setValidity() would need to be called explicitly as the penultimate line of an initialize method or constructor.
Ok so you can do this in initialize. It should have the form:
initialize = function (...) {
if (nargs()) return ()
# Capture arguments in list
args <- list(...)
# If the field name is passed to the initialize function
# then check whether it is valid and assign it. Otherwise
# assign a zero length value (character if field_name has
# that type)
if (!is.null(args$field_name)) {
assert_that(check_field_name(args$field_name))
field_name <<- field_name
} else {
field_name <<- character()
}
# Make sure you callSuper as this will then assign other
# fields included in ... that weren't already specially
# processed like `field_name`
callSuper(...)
}
This is based on the strategy set out in the lme4 package.
I'm aware that this would be a terribly unreliable hack. But out of pure interest:
What would you need to manually change in the .refClassDef field of an ref class object if the Reference Class definition of an already instantiated object changed and you would like it to "be informed about the update" (without re-instantiating it).
After all, it does seem to work if additional methods are introduced, but not for modifications of existing methods (see example below).
This question is related to my answer in this post.
Example
Original class def:
MyReferenceClass <- setRefClass("MyReferenceClass",
methods = list(
print_hello = function(){
print("hello")
}
)
)
Instantiate:
my_object <- MyReferenceClass$new()
my_object$print_hello()
[1] "hello"
Updated class def:
MyReferenceClass <- setRefClass("MyReferenceClass",
methods = list(
print_hello = function(){
print("hello_again")
},
print_goodbye = function(){
print("goodbye")
}
)
)
Instance can use the new method:
my_object$print_goodbye()
[1] "goodbye"
But it would for example fail to be informed about changes in print_hello as this post illustrates.
Include an update method in your class (before and after modification). Call obj$update() before calling another method upon any updates of the class.
MyReferenceClass <- setRefClass(
"MyReferenceClass",
methods = list(
print_hello = function(){
print("hello")
},
update = function(){
selfEnv <- as.environment(.self)
.class <- as.character(class(.self))
for (e in as.vector(utils::lsf.str(selfEnv))){
v <- get(e,eval(parse(text=sprintf("%s#generator$def#refMethods",.class))))
assign(e,v,envir=selfEnv)
}
}
)
)
UPDATE: Wed Jun 11 The update method now asks for a vector of methods to be updated, to avoid accidentally changes of internal methods. Thanks to #Rappster for pointing this out.
MyReferenceClass <- setRefClass(
"MyReferenceClass",
methods = list(
print_hello = function(){
print("hello")
},
update = function(x){
selfEnv <- as.environment(.self)
.class <- as.character(class(.self))
for (e in x){
v <- get(e,eval(parse(text=sprintf("%s#generator$def#refMethods",.class))))
assign(e,v,envir=selfEnv)
}
}
)
)
obj <- MyReferenceClass$new()
obj$print_hello()
## ...
obj$update('print_hello')