Background
Say, I have a ClassA
and ClassB
, each of which require their own arguments in the respective constructor functions:
ClassA <- function(A_arg1, A_arg2) {
# some class-SPECIFIC construction magic happens, say
out <- list(A_arg1, A_arg2)
# some GENERAL construction magic happens
class(out) <- "ClassA"
return(out)
}
ClassB <- function(B_arg1, B_arg2) {
# some class-SPECIFIC construction magic happens, say
out <- B_arg1 + B_arg2
# some GENERAL construction magic happens
class(out) <- "ClassB"
return(out)
}
My actual use case is a bit more complicated; I'm trying to use constructor and check.ClassName()
methods to shoehorn type validation into S3
.
Motivation
Obviously, I'd love to avoid the duplication in the general part of the above constructor functions, so a function factory that could be used like so would be nice:
ClassA <- produce_class_constructor(classname = "ClassA", fun = function(A_arg1, A_arg2) {return(list(A_arg1, A_arg2))})
This should, ideally, yield the exact same function as the above manually constructed ClassA
function, with the general part factored out.
(The general part in my real use case is quite a bit longer).
Caveat: It's crucial that the factory-produced ClassA()
constructor retain the argument names (here A_arg1
, A_arg2
), so that documentation and autocomplete can guide the user in construction.
I want to use best functional programming (FP) practices to solve this problem.
I've previously asked about doing this via a simple closure, but I know wonder whether doing this via function operators might not be a better way, where I simply wrap (or compose
) the general operators around the particular pieces for each class to yield a constructor function for each class.
In pseudocode, I'd like to design something like this:
produce_class_constructor <- function(classname, fun) {
class_constructor_func <- validate_class(append_class(classname, fun()))
}
where, append_class()
makes the custom fun
also append some class, and validate_class()
in turn also makes the custom fun
run checks on its results.
I could then use this constructor function factor to elegantly create, say, the above ClassA()
:
ClassA <- produce_class_constructor(classname = "ClassA", fun = function(A_arg1, A_arg2) {return(list(A_arg1, A_arg2))})
Attempt
Here's my simple (failing) attempt.
# simple helper to append classes
append_class <- function(obj, classname) {
class(obj) <- append(class(obj), classname)
return(obj)
}
produce_class_constructor <- function(classname, fun) {
force(fun) # not sure what this does, but seems to be necessary as per http://adv-r.had.co.nz/Function-operators.html
# all the GENERAL construction magic from above now happens here
class_constructor_fun <- function(...) {
append_class(classname, fun(...))
}
formals(class_constructor_fun) <- formals(fun) # this is necessary to carry over argument names
return(class_constructor_fun)
}
ClassB <- produce_class_constructor(classname = "ClassB", fun = function(B_arg1, B_arg2) {
# this is the class-SPECIFIC construction magic from above
out <- B_arg1 + B_arg2
return(out)
})
ClassB(B_arg1 = 1, B_arg2 = 2) # this fails
The good news is that the resulting ClassB()
has named arguments.
The bad news is that they don't get passed along, and the whole thing fails with:
Error in append(class(obj), classname) :
'...' used in an incorrect context
Question(s)
- Is this a sound "point-free" FP design and an ok/good idea generally?
- If yes, how can I fix my attempt? (= make the "inner" function aware of its arguments)
- If not, what would be the "proper" FP way of doing this?
- (Bonus): Is there already a package out there that shoehorns this kind of validation and rigour into
S3
? (other OOs are not an option and scare my pants off).