Documenting R6 classes and methods within R package in RStudio
Asked Answered
M

2

17

I am struggling with the documentation of an R6 class and its methods. My goal is to get the autocompletion in RStudio for the methods. At the moment, I only get the name of the method but no the help information I normally get using roxygen2 documenting a function with parameters etc.

At the moment, this is my class:

#' @importFrom R6 R6Class
MQParameters <- R6::R6Class(
  'MQParameters',
  public=list(
    initialize=function(file_path=NA) {
      private$location <- file_path
      mq_parameters <- read.delim(file_path, stringsAsFactors=FALSE)
      mq_parameters <-
        setNames(mq_parameters$Value, mq_parameters$Parameter)
      private$mq_version <- unname(mq_parameters['Version'])
      private$fasta_file <-
        gsub('\\\\', '/', strsplit(mq_parameters['Fasta file'], ';')[[1]])
    },
    # this method returns the version
    getVersion=function() {
      private$mq_version
    },
    # this methods returns the fastafile.
    # @param new_param it is possible to rewrite the basedir.
    getFastaFile=function(new_basedir=NA) {
      if(is.na(new_basedir)) {
        private$fasta_file
      } else {
        file.path(new_basedir, basename(private$fasta_file))
      }
    }
  ),
  private=list(
    location=NULL,
    mq_version=NULL,
    fasta_file=NULL
  )
)

If you are interested to test this class, here is a little reproducible example:

df <- data.frame(Parameter=c('Version', 'Fasta file'),
                 Value=c('1.5.2.8','c:\\a\\b.fasta'))
write.table(df, 'jnk.txt', sep='\t', row.names=F)

p <- MQParameters$new('jnk.txt')
p$getVersion()
# [1] "1.5.2.8"
p$getFastaFile()
# [1] "c:/a/b.fasta"
p$getFastaFile(new_basedir='.')
# [1] "./b.fasta"

I don't know how to document parameters, because the parameters are actually belong to the creator but not to the class. What about the parameters to other methods within the function?
What is the preferred way to document a class with it's methods?

I would love to get the "normal" functionality from RStudio, like hitting F1 to get directly to the help page.

By searching the internet, I saw already some reports on Github about this topic, but they more than a year old.

Update

Thanks to the answer of mikeck I now have a nice documentation for the class and it's methods. But what I am still lacking is the possibility to get the hint of function/method and its arguments like in this screenshot for a common function:

rStudio help on functions

I am wondering if I can somehow register my function manually, but since it doesn't have a specific name (it is always coupled with the variable objectname you use for the object OBJECTNAME$methodeCall()) I don't know how to do this.

Mcfall answered 1/8, 2017 at 7:45 Comment(0)
S
12

My understanding is that it is easiest to document a NULL object with the same @name as your class, as this provides maximum flexibility. I use an R6 class in one of my packages; you can view the roxygen here. I've included a small sample below:

#' Python Environment
#' 
#' The Python Environment Class. Provides an interface to a Python process.
#' 
#' 
#' @section Usage:
#' \preformatted{py = PythonEnv$new(port, path)
#'
#' py$start()
#' 
#' py$running
#' 
#' py$exec(..., file = NULL)
#' py$stop(force = FALSE)
#' 
#' }
#'
#' @section Arguments:
#' \code{port} The port to use for communication with Python.
#' 
#' \code{path} The path to the Python executable.
#' 
#' \code{...} Commands to run or named variables to set in the Python process.
#'
#' \code{file} File containing Python code to execute.
#' 
#' \code{force} If \code{TRUE}, force the Python process to terminate
#'   using a sytem call.
#' 
#' @section Methods:
#' \code{$new()} Initialize a Python interface. The Python process is not 
#'   started automatically.
#'   
#' \code{$start()} Start the Python process. The Python process runs 
#'   asynchronously.
#'
#' \code{$running} Check if the Python process is running.
#'   
#' \code{$exec()} Execute the specified Python 
#'   commands and invisibly return printed Python output (if any).
#'   Alternatively, the \code{file} argument can be used to specify
#'   a file containing Python code. Note that there will be no return 
#'   value unless an explicit Python \code{print} statement is executed.
#' 
#' \code{$stop()} Stop the Python process by sending a request to the 
#'   Python process. If \code{force = TRUE}, the process will be 
#'   terminated using a system call instead.
#'
#' @name PythonEnv
#' @examples
#' pypath = Sys.which('python')
#' if(nchar(pypath) > 0) { 
#'   py = PythonEnv$new(path = pypath, port = 6011)
#'   py$start()
#'   py$running
#'   py$stop(force = TRUE)
#' } else 
#' message("No Python distribution found!")
NULL

#' @export
PythonEnv = R6::R6Class("PythonEnv", cloneable = FALSE,
  # actual class definition...

There are other alternative (but similar) implementations; this example uses @docType class which might suit you better:

#' Class providing object with methods for communication with lightning-viz server
#'
#' @docType class
#' @importFrom R6 R6Class
#' @importFrom RCurl postForm
#' @importFrom RJSONIO fromJSON toJSON
#' @importFrom httr POST
#' @export
#' @keywords data
#' @return Object of \code{\link{R6Class}} with methods for communication with lightning-viz server.
#' @format \code{\link{R6Class}} object.
#' @examples
#' Lightning$new("http://localhost:3000/")
#' Lightning$new("http://your-lightning.herokuapp.com/")
#' @field serveraddress Stores address of your lightning server.
#' @field sessionid Stores id of your current session on the server.
#' @field url Stores url of the last visualization created by this object.
#' @field autoopen Checks if the server is automatically opening the visualizations.
#' @field notebook Checks if the server is in the jupyter notebook mode.
#' #' @section Methods:
#' \describe{
#'   \item{Documentation}{For full documentation of each method go to https://github.com/lightning-viz/lightining-r/}
#'   \item{\code{new(serveraddress)}}{This method is used to create object of this class with \code{serveraddress} as address of the server object is connecting to.}
#'
#'   \item{\code{sethost(serveraddress)}}{This method changes server that you are contacting with to \code{serveraddress}.}
#'   \item{\code{createsession(sessionname = "")}}{This method creates new session on the server with optionally given name in \code{sessionname}.}
#'   \item{\code{usesession(sessionid)}}{This method changes currently used session on the server to the one with id given in \code{sessionid} parameter.}
#'   \item{\code{openviz(vizid = NA)}}{This method by default opens most recently created by this object visualization. If \code{vizid} parameter is given, it opens a visualization with given id instead.}
#'   \item{\code{enableautoopening()}}{This method enables auto opening of every visualisation that you create since that moment. Disabled by default.}
#'   \item{\code{disableautoopening()}}{This method disables auto opening of every visualisation that you create since that moment. Disabled by default.}
#'   \item{\code{line(series, index = NA, color = NA, label = NA, size = NA, xaxis = NA, yaxis = NA, logScaleX = "false", logScaleY = "false")}}{This method creates a line visualization for vector/matrix with each row representing a line, given in \code{series}.}
#'   \item{\code{scatter(x, y, color = NA, label = NA, size = NA, alpha = NA, xaxis = NA, yaxis = NA)}}{This method creates a scatterplot for points with coordinates given in vectors \code{x, y}.}
#'   \item{\code{linestacked(series, color = NA, label = NA, size = NA)}}{This method creates a plot of multiple lines given in matrix \code{series}, with an ability to hide and show every one of them.}
#'   \item{\code{force(matrix, color = NA, label = NA, size = NA)}}{This method creates a force plot for matrix given in \code{matrix}.}
#'   \item{\code{graph(x, y, matrix, color = NA, label = NA, size = NA)}}{This method creates a graph of points with coordinates given in \code{x, y} vectors, with connection given in \code{matrix} connectivity matrix.}
#'   \item{\code{map(regions, weights, colormap)}}{This method creates a world (or USA) map, marking regions given as a vector of abbreviations (3-char for countries, 2-char for states) in \code{regions} with weights given in \code{weights} vector and with \code{colormap} color (string from colorbrewer).}
#'   \item{\code{graphbundled(x, y, matrix, color = NA, label = NA, size = NA)}}{This method creates a bundled graph of points with coordinates given in \code{x, y} vectors, with connection given in \code{matrix} connectivity matrix. Lines on this graph are stacked a bit more than in the \code{graph} function.}
#'   \item{\code{matrix(matrix, colormap)}}{This method creates a visualization of matrix given in \code{matrix} parameter, with its contents used as weights for the colormap given in \code{colormap} (string from colorbrewer).}
#'   \item{\code{adjacency(matrix, label = NA)}}{This method creates a visualization for adjacency matrix given in \code{matrix} parameter.}
#'   \item{\code{scatterline(x, y, t, color = NA, label = NA, size = NA)}}{This method creates a scatterplot for coordinates in vectors \code{x, y} and assignes a line plot to every point on that plot. Each line is given as a row in \code{t} matrix.}
#'   \item{\code{scatter3(x, y, z, color = NA, label = NA, size = NA, alpha = NA)}}{This method creates a 3D scatterplot for coordinates given in vectors \code{x, y, z}.}
#'   \item{\code{image(imgpath)}}{This method uploads image from file \code{imgpath} to the server and creates a visualisation of it.}
#'   \item{\code{gallery(imgpathvector)}}{This method uploads images from vector of file paths \code{imgpathvector} to the server and creates a gallery of these images.}}


Lightning <- R6Class("Lightning",
...
)

EDIT

If you are looking for a way to get the RStudio tooltips to show up when trying to use a class method... unfortunately I don't think you will find a solution that doesn't require coding your classes in a way that eliminates the convenience and functionality of R6 classes.

@f-privé has provided an answer that will do what you want---just extend that logic to ALL methods. For example, myclass$my_method is instead accessed by

my_method = function(r6obj) {
  r6obj$my_method()
}
obj$my_method()
my_method(obj)      # equivalent

In other words, you would need to create a wrapper for each method. This obviously is less convenient to program than just using the obj$my_method(), and probably kills the usefulness of using an R6 class in the first place.

The issue here is really RStudio. The IDE doesn't have a good way of identifying R6 classes by analyzing the code, and can't distinguish between methods of a defined class and elements of a list or environment. Furthermore, RStudio can't provide help on arbitrary functions, like:

na.omit()         # tooltip shows up when cursor is within the parentheses
foo = na.omit
foo()             # no tooltip

which is fairly analogous to calling methods of a particular R6 object.

Ski answered 10/8, 2017 at 1:25 Comment(5)
Thanks, this gives me already an nice documentation within the help! What I am still looking for is to get the help with rStudio. What I would love to have is the autocomplete and the little yellow window showing the function call with its arguments. Right now I get the function name completed but no hint on the arguments. I have to search via ?MQParameters to get the help and have to read through the methods section.Mcfall
@Mcfall I edited my answer to discuss the tooltip issue, but unfortunately I don't think you will find a satisfactory solution with the current version of RStudio.Ski
@Ski I don't remember how I ended up with such approach, but you can check examples in text2vec - github.com/dselivanov/text2vec/blob/master/R/model_LSA.R. I just remember that I also had issues with finding best way to document R6.Procora
@Ski I was hoping that someone of the rStudio community would give a little hint on how this would be possible, but I guess I have to write to them directly.Mcfall
Unfortunately, using @docType class now seems to result in the error Error: $ operator is invalid for atomic vectors. Looks like it might have atrophied.Skeptic
W
7

I think R people don't want to use $new(...) to get an instance of a new class. They prefer to have a function with the same name of the class to construct an instance of it.

So, what you could do is rename your R6ClassGenerator MQParameters_R6Class and create another function

MQParameters <- function(file_path = NA) {
  MQParameters_R6Class$new(file_path)
}

Then, document this function as any other function and you will get "the little yellow window showing the function call with its arguments" from RStudio. And happy R users.

Wizened answered 14/8, 2017 at 14:15 Comment(1)
I like the idea but I am still not happy with it! I think it is the better way to program! :-) Lets see if there are better suggestions, but thanks I might consider it.Mcfall

© 2022 - 2024 — McMap. All rights reserved.