0
votes

I would like to use multiple constructors in my R S4 class.

I have an object that has three slots. To make that object, sometimes I want to just give the values for the three slots outright. But sometimes I'd like to provide a matrix, and I have a function that can take a matrix and return what those three slots should be.

At first, it seems like I could write a function as a constructor. So I could write objectFromMatrix(matrix) --> object with three slots. The problem is that I also have sub-classes that inherit from that main class, and I want to be able to use that constructor with them as well.

So I could just write functions as extra constructors for each of the subclasses, but that would be a bit tedious and not super OO-like.

To make my problem a little more tangible, I'll try to write a minimal example below. I'll write it in Java, but I'm a bit rusty so let me know if it doesn't make sense.


Desired structure, in Java:

// An abode is a place where you live and it has a size
class Abode {
    int size = 1;

    // Main constructor that just assigns args to fields
    Abode(int size) {
        this.size = size;
    }

    // Alternative constructor that takes in a different datatype
    // and computes args to assign to fields
    Abode(string description) {
        if(description eq "Large") {
            this.size = 5;
        }

        if(description eq "Small") {
            this.size = 1;
        }
}

// To keep it simple, a house is just an abode with a name
class House extends Abode {
        String name;

        House(int size, String name) {
                super(size);
                this.name = name;
        }

        House(string size, String name) {
                super(size);
                this.name = name;
        }
}

This implementation works nicely because I can call Abode("big") or House("big", "Casa de me"), and both of those get passed to the extra constructor I built in the Abode class.


Keeping up with the house analogy, this is the best I've been able to do in R:

# An abode is a place you live and it has a size
setClass("Abode",
         slots = 
             list(size = "numeric")
)

# Alternative constructor that takes in a different datatype
# and computes args to assign to fields
abode_constructor_2 <- function(sizeString) {
    if (sizeString == "big") {return new("Abode", size = 5)}
    if (sizeString == "small") {return new("Abode", size = 1)}
}

# A house is an abode with a name
setClass("House",
         slots = 
             list(name = "string"),
         contains = "Abode"
)

# I already defined this constructor but I have to do it again
house_constructor_2 <- function(sizeString, name) {
    if (sizeString == "big") {return new("House", size = 5, name = name)}
    if (sizeString == "small") {return new("House", size = 1, name = name)}
}

In case it helps, here is a minimal example of the real context where this problem is coming up. I define an extra constructor for the Sensor class, sensor_constructor_2, as a function. But then, when I have a class that inherits from Sensor, I have to make that constructor over again.

# A sensor has three parameters
setClass("Sensor",
         slots = 
             list(Rmin = "numeric", Rmax = "numeric", delta = "numeric")
)

# I also like to make sensors from a matrix
sensor_constructor_2 <- function(matrix) {
    params <- matrix_to_params(matrix)
    return (new("Sensor", Rmin = params[1], Rmax = params[2], delta = params[3]))
}


# A redoxSensor is just a sensor with an extra field
setClass("redoxSensor",
         slots = 
             list(e0 = "numeric"),
         contains = "Sensor"
)

# Goal: make this extra constructor unnecessary by making sensor_constructor_2 a property of the sensor class
extraConstructor_redox <- function(matrix, e0) {
    params <- matrix_to_params(matrix)
    return (new("redoxSensor", Rmin = params[1], Rmax = params[2], delta = params[3]), e0 = e0)
}
1
Consider first whether you really need S4. In my experience, S3 gets the job done 99% of the time with far less complexityHong Ooi
Absolutely, that makes sense. I'm choosing to use S4 here as a personal preference--I'm not super confident with OO and I think the extra structure will help me stay away from design mistakes, but I'll think more about using S3. Either way, if you (or anyone reading) can answer my question using S3 classes, please do!julianstanley

1 Answers

0
votes

There is no reason why you can't do this with one S4 constructor by using default arguments and a little extra logic, along the lines of

setClass("Abode",
  slots = list(size = "numeric")
) -> Abode

setClass("House",
  slots = list(name = "character"),
  contains = "Abode"
) -> House

createDwelling <- function(size=0,name,sizeString){
  if(!missing(sizeString)){
    if(sizeString == "Large") size <- 5
    else if(sizeString == "Small") size <- 1
    else stop("invalid sizeString")
  }
  if(missing(name)) return(Abode(size=size))
  else return(House(size=size,name=name))
}

example usage:

> createDwelling(size=3)
An object of class "Abode"
Slot "size":
[1] 3

> createDwelling(sizeString="Small")
An object of class "Abode"
Slot "size":
[1] 1

> createDwelling(sizeString="Small",name="my house")
An object of class "House"
Slot "name":
[1] "my house"

Slot "size":
[1] 1