Search code examples
rmethodsprintingconsoler-package

How to custom print/show variables (with custom class) in my R package


Many R functions return objects that are printed to the console in a special manner. For instance, t_results = t.test(c(1,2,3), c(1,2,4)) will assign a list to the t_results variable, but when I enter this variable in the console, or call it as print(t_results) or show(t_results), it prints some plain text information (such as Welch Two Sample t-test... etc.) instead of returning the actual list. (This is a base R function, but I've seen this implemented in many custom user R packages just as well.)

My question is: how do I do this for objects created in my own custom R package? I've read several related questions and answers (e.g., this, this, and this), which do give a general idea (using setMethod for my custom classes), but none of them makes it clear to me what exactly I need to do to make it work properly in a custom R package. I also cannot find any official documentation or tutorial on the matter.

To give an example of what I want to do, here is a very simple function from my hypothetical R package, which simply return a small data.frame (with an arbitrary class name I add to it, here 'my_df_class'):

my_main_function = function() {
    my_df = data.frame(a = c('x1', 'y2', 'z2'),
                       b = c('x2', 'y2', 'z2'))
    class(my_df) = c(class(my_df), 'my_df_class')
    return(my_df)
}

I would like to have this printed/shown e.g. like this:

my_print_function = function(df) {
    cat('My results:', df$a[2], df$a[3])
}
# see my_print_function(my_main_function())

What exactly has to be done to make this work for my R package (i.e., that when someone installs my R package, assigns the my_main_function() results to a variable, and prints/shows that variable, it would be done via my_print_function())?


Solution

  • Here is a small explanation. Adding to the amazing answer posted by @nya:

    First, you are dealing with S3 classes. With these classes, we can have one method manipulating the objects differently depending on the class the object belongs to.

    Below is a simple class and how it operates:

    1. Class contains numbers,
    2. The class values to be printed like 1k, 2k, 100k, 1M,
    3. The values can be manipulated numerically.

    -- Lets call the class my_numbers

    Now we will define the class constructor:

     my_numbers = function(x) structure(x, class = c('my_numbers', 'numeric'))
    

    Note that we added the class 'numeric'. ie the class my_numbers INHERITS from numeric class

    We can create an object of the said class as follows:

    b <- my_numbers(c(100, 2000, 23455, 24567654, 2345323))
    b 
    [1]      100     2000    23455 24567654  2345323
    attr(,"class")
    [1] "my_numbers" "numeric" 
    

    Nothing special has happened. Only an attribute of class has been added to the vector. You can easily remove/strip off the attribute by calling c(b)

    c(b)
    [1]      100     2000    23455 24567654  2345323
    

    vector b is just a normal vector of numbers.

    Note that the class attribute could have been added by any of the following (any many more ways):

     class(b) <- c('my_numbers', 'numeric')
     attr(b, 'class') <- c('my_numbers', 'numeric')
     attributes(b) <- list(class = c('my_numbers', 'numeric'))
    

    Where is the magic?

    I will write a simple function with recursion. Don't worry about the function implementation. We will just use it as an example.

    my_numbers_print = function(x, ..., digs=2,  d = 1,  L =   c('', 'K', 'M', 'B', 'T')){
      ifelse(abs(x) >= 1000, Recall(x/1000, d = d + 1),
             sprintf(paste0('%.',digs,'f%s'), x, L[d]))
    }
    
    my_numbers_print(b)
    [1] "100.00" "2.00K"  "23.45K" "24.57M" "2.35M" 
    

    There is no magic still. Thats the normal function called on b.

    Instead of calling the function my_numbers_print we could write another function with the name print.my_numbers ie method.class_name (Note I added the parameter quote = FALSE

    print.my_numbers = function(x, ..., quote = FALSE){
       print(my_numbers_print(x), quote = quote)
     }
       
     b
    [1] 100.00 2.00K  23.45K 24.57M 2.35M 
    

    Now b has been printed nicely. We can still do math on b

     b^2
     [1] 10.00K  4.00M   550.14M 603.57T 5.50T 
    

    Can we add b to a dataframe?

    data.frame(b)
             b
    1      100
    2     2000
    3    23455
    4 24567654
    5  2345323 
    

    b reverts back to numeric instead of maintaining its class. That is because we need to change another function. ie the formats function.

    Ideally, the correct way to do this is to create a format function and then the print function. (Becoming too long)


    Summary : Everything Put Together

    # Create a my_numbers class definition function
    my_numbers = function(x) structure(x, class = c('my_numbers', 'numeric'))
    
    # format the numbers
    format.my_numbers =  function(x,...,digs =1, d = 1,  L =   c('', 'K', 'M', 'B', 'T')){
          ifelse(abs(x) >= 1000, Recall(x/1000, d = d + 1),
             sprintf(paste0('%.',digs,'f%s'), x, L[d]))
    }
    
    #printing the numbers
    print.my_numbers = function(x, ...) print(format(x), quote = FALSE)
    
    # ensure class is maintained after extraction to allow for sort/order etc
    '[.my_numbers' = function(x, ..., drop = FALSE)  my_numbers(NextMethod('['))
    
    
    b <- my_numbers(c(2000, 100, 20, 23455, 24567654, 2345323))
    
    data.frame(x = sort(-b) / 2)                     
       
           x
    1 -12.3M
    2  -1.2M
    3 -11.7K
    4  -1.0K
    5  -50.0
    6  -10.0