15 R code
Everyone has their own coding style and formats. There are however some best practice guidelines that Bioconductor reviewers will look for.
can be a robust, fast and efficient programming language but often some coding practices result in less than ideal use of resources.
This section will review some key points, suggestions, and best practices that will aid in the package review process and assist in making code more robust and efficient.
15.1 License
Only contain code that can be distributed under the license specified (see also The DESCRIPTION file).
15.2 R Code Development
15.2.1 Re-use of functionality, classes, and generics
Avoid re-implementing functionality or classes (see also The DESCRIPTION file). Make use of appropriate existing packages (e.g., biomaRt, AnnotationDbi, Biostrings, GenomicRanges) and classes (e.g., SummarizedExperiment, Biobase::AnnotatedDataFrame, GenomicRanges:;GRanges, Biostrings::DNAStringSet) to avoid duplication of functionality available in other Bioconductor packages. See also Common Bioconductor Methods and Classes.
This encourages interoperability and simplifies your own package development. If a new representation is needed, see class development section. In general, Bioconductor will insist on interoperability with Common Classes for acceptance.
Developers should make an effort to re-use generics that fit the generic
contract for the proposed class-method pair i.e., the behavior of the method
aligns with the originally proposed behavior of the generic. Specifically,
the behavior can be one where the return value is of the same class across
methods. The method behavior can also be a performant conceptual transformation
or procedure across classes as described by the generic.
BiocGenerics lists commonly used generics in
Bioconductor. One example of a generic and method implementation is that of the
rowSums
generic and the corresponding method within the
DelayedArray package. This generic contract returns a
numeric vector of the same length as the rows and is adhered to across classes
including the DelayedMatrix
class. Re-using generics reduces the amount of
new generics by consolidating existing operations and avoids the mistake of
introducing a “new” generic with the same name. Generic name collisions may
mask or be masked by previous definitions in ways that are hard to diagnose.
More recently, we recommend the use of conflicted to
identify namespace collisions.
If there are problems, e.g., in performance or parsing your particular
file type, ask for input from other developers on the
bioc-devel mailing list. Common disadvantages to
‘implementing your own’ are the introduction of non-standard data
representations (e.g., neglecting to translate coordinate systems of file
formats to Bioconductor objects) and user bewilderment. Therefore, in such case,
we recommend to use standard file format classes inheriting from
BiocIO’s BiocFile
class in packages such as
rtracklayer and BiocIO.
15.2.2 Naming of packages, functions, and classes
See section on package naming. The concepts there should also apply to function names, class names, etc.
Avoid names that:
- Are easily confused with existing package names, function names, class names.
- Imply a temporal (e.g.
ExistingPackage2
) or qualitative (e.g.ExistingPackagePlus
) relationship. - Suggest hate speech, slurs or profanity, either implicitly or explicitly.
- Invoke or refer to any historical, ethical, or political contexts.
- Reference well known people, characters, brands, places or icons.
15.2.3 Methods development
We encourage maintainers to only create new methods for classes exported within
their packages. We discourage the generation of methods for external classes,
i.e., classes outside of the package NAMESPACE
. This can
potentially cause method name collisions (i.e., where two methods defined on
the same object but in different packages) and pollute the methods environment
for those external classes. New methods for established classes can also cause
confusion among users given that the new method and class definition are in
separate packages.
15.2.4 File names
- Filename extension for R code should be ‘.R’. Use the prefix ‘methods-’ for S4 class methods, e.g., ‘methods-coverage.R’. Generic definitions can be listed in a single file, ‘AllGenerics.R’, and class definitions in ‘AllClasses.R’.
- Filename extension for man pages should be ‘.Rd’.
15.2.5 Class development
Remember to re-use common Bioconductor methods and classes before implementing new representations. This encourages interoperability and simplifies your own package development.
15.2.5.2 Class design
Bioconductor prefers the use of S4 classes over S3 class. Please use the S4 representation for designing new classes. Again, we can not emphasize enough to reuse existing class structure if at all possible. If an existing class structure is not sufficient, it is also encouraged to look at extending or containing an existing Bioconductor class.
If your data requires a new representation or function, carefully design an S4 class or generic so that other package developers with similar needs will be able to re-use your hard work, and so that users of related packages will be able to seamlessly use your data structures. Do not hesitate to ask on the Bioc-devel mailing list for advice.
For any class you define, implement and use a ‘constructor’ for object creation. A constructor is usually plain-old-function (rather than, e.g., a generic with methods). It provides documented and user-friendly arguments, while allowing for developer-friendly implementation. Use the constructor throughout your own code, examples, and vignette.
Implement a show()
method to effectively convey information to your
users without overwhelming them with detail.
Accessors (simple functions that return components of your object)
rather than direct slot access (using @
) help to isolate the
implementation of your class from its interface. Generally @
should
only be used in an accessor, all other code should use the
accessor. The accessor does not need to be exported from the class if
the user has no need or business accessing parts of your data
object. Plain-old-functions (rather than generic + method) are often
sufficient for accessors; it’s often useful to employ (consistently) a
lightweight name mangling scheme (e.g., starting the accessor method
name with a 2 or 3 letter acronym for your package) to avoid name
collisions between similarly named functions in other packages.
The following layout is sometimes used to organize classes and methods; other approaches are possible and acceptable.
- All class definitions in R/AllClasses.R
- All generic function definitions in R/AllGenerics.R
- Methods are defined in a file named by the generic function. For
example, all
show
methods would go in R/show-methods.R.
A Collate: field in the DESCRIPTION file may be necessary to order class and method definitions appropriately during package installation.
15.2.6 Function development
15.2.6.1 Functions Names
Function names should follow the following conventions:
- Use camelCase: initial lower case, then alternate case between words.
- Do not use ‘.’ (in the S3 class system,
some(x)
wherex
is classA
will dispatch tosome.A
). - Prefix non-exported functions with a ‘.’.
15.2.6.2 Functional Programming and Length
Guiding principle: Smaller functions are easier to read, test, debug and reuse.
Avoid large chunks of repeated code. If code is being repeated this is generally a good indication a helper function could be implemented.
Excessively long functions should also be avoided. Write small functions.
It is best if each function has only one job that it needs to do. And it is also best if that function does that job in as few lines of code as possible. If you find yourself writing great long functions that extend for more than a screen, then you should probably take a moment to split it up into smaller helper functions.
Nesting functions should be avoiding. It unnecessarily increases the main function length and makes the main function harder to read. Helper functions and internal functions should avoid being nested and include at the end of the file or as a seperate file (e.g. internal.R, helpers.R, utils.R)
15.2.6.3 Function arguments
- Argument names to functions should be descriptive and well documented.
- Arguments should generally have default values.
- Check arguments against a validity check or
stopifnot
15.2.6.5 Additional files and dependencies
Do NOT install anything on a users system.
System dependencies, applications, and additionally needed packages should be assumed already present on the user’s system.
If necessary, package maintainers should provide instructions for download and setup, but should not execute those instructions on behalf of a user. Complicated or additional system dependency instructions could be part of the README file and/or INSTALL file. All package dependencies must actively be on CRAN or Bioconductor.
All system and package dependencies should be the latest publically available version.
15.2.7 Coding Style
While it may seem arbitrary, being consistent with coding style helps other understand, evaluate, and debug code easier. While these are optional, it is highly encouraged.
15.2.7.2 Use of space
- Always use space after a comma. This:
a, b, c
. - No space around “=” when using named arguments to functions. This:
somefunc(a=1, b=2)
- Space around all binary operators:
a == b
.
15.2.7.3 Comments
- Use “##” to start full-line comments.
- Indent at the same level as surrounding code.
- Comments should be for clarification, notes, and documentation only.
- Do not leave in commented out code chunks that are not utilized
- Commenting TODO’s should be avoided in published package code
15.2.7.4 Additonal style and formatting references
formatR a package that assists with formatting.
15.3 R Code Best Practices and Guidelines
Many common coding and syntax issues are flagged in R CMD check
and
BiocCheck()
(see the R CMD check
cheatsheet and BiocCheck vignette. Every
effort should be made to clear up ERROR, WARNING, or NOTEs produced from these
checks.
15.3.1 Code syntax and efficiency
- Use
vapply()
instead ofsapply()
, and use the variousapply
functions instead offor
loops. See below section on vectorize. - Use
seq_len()
orseq_along()
instead of1:...
. See below section on avoid1:n
style iterations - Use
TRUE
andFALSE
instead ofT
andF
. - Use numeric indices at lower level interfaces (e.g., internal subset ops) for computational efficiency.
- Use named indices at higher level interfaces (e.g., function arguments) for robustness.
- Use
is()
instead ofclass() ==
andclass() !=
. - Use
system2()
instead ofsystem()
. - Do not use
set.seed()
in any internal code. - Do not use
browser()
in any internal code. - Avoid the use of
<<-
. - Avoid use of direct slot access with
@
orslot()
. Accessor methods should be created and utilized - Use the packages ExperimentHub and AnnotationHub instead of downloading external data from unsanctioned providers such as GitHub, <i * class=“fa fa-dropbox” aria-hidden=“true”> Dropbox, etc. In general, data utlilized in packages should be downloaded from trusted public databases. See also section on web querying and file caching.
- Use
<-
instead of=
for assigning variables except in function arguments.
15.3.2 Cyclomatic Complexity
A metric developed by Thomas J. McCabe, cyclomatic complexity describes the control-flow of a program. The higher the value, the more complex the code is said to be.
In the BiocCheck
example below, we have a series of control-flow statements
that check for multiple conditions and return a value if some of those
conditions are true. The function hasValueSection
is checking that the
documentation contains a ‘value’ Rd tag i.e., a return section in the
documentation.
hasValueSection <- function(manpage) {
rd <- tools::parse_Rd(manpage)
type <- BiocCheck:::docType(rd)
if (identical(type, "data"))
return(TRUE)
tags <- tools:::RdTags(rd)
if ("\\usage" %in% tags && (!"\\value" %in% tags))
return(FALSE)
value <- NULL
if ("\\value" %in% tags)
value <- rd[grep("\\value", tags)]
if ("\\usage" %in% tags && (!"\\value" %in% tags)) {
values <- paste(unlist(value), collapse='')
test <- (is.list(value[[1]]) && length(value[[1]]) == 0) ||
nchar(gsub(" ", "", values)) == 0
if (test)
return(FALSE)
}
TRUE
}
As you can see, the code here is quite complex, creating if
statements for
specific conditions. The measured cyclomatic complexity as given by the
cyclocomp
package is a value of 16.
Let’s re-write the function to reduce the complexity and use a base character vector representation.
hasValueSection2 <- function(manpage) {
rd <- tools::parse_Rd(manpage)
tags <- tools:::RdTags(rd)
value <- rd[grepl("\\value", tags)]
value <- unlist(value, recursive = FALSE)
value <- Filter(function(x) attr(x, "Rd_tag") != "COMMENT", value)
values <- paste(value, collapse='')
nzchar(trimws(values)) && length(value)
}
By internalizing a base character vector representation in the code, the successive functions will operate on that representation. This gives us code that is less complex and easier to understand and maintain with a complexity metric value of 2.
cyclocomp(hasValueSection2)
#> [1] 2
Note. Switches based on docType
should be outside of the function as a filter
for the manpage
input instead.
15.3.2.1 End-User messages
Use the functions message()
, warning()
and error()
, instead of the
cat()
function (except for customized show()
methods). paste0()
should
generally not be used in these methods except for collapsing multiple values
from a variable.
15.3.2.2 Graphic Device
Use dev.new()
to start a graphics drive if necessary. Avoid using x11()
or X11()
, for it can only be called on machines that have access to an X
server.
15.3.2.3 Web Querying and File caching
Files downloaded should be cached.
Please use BiocFileCache.
If a maintainer creates their own caching directory, it should utilize standard
caching directories tools::R_user_dir(package, which="cache")
. It is not
allowed to download or write any files to a users home directory, working
directory, or installed package directory. Files should be cached as stated
above with BiocFileCache
(preferred) or R_user_dir
or tempdir()/tempfile()
if files should not be persistent.
Please also follow guiding principles in Appendix section Querying Web Resources, if applicable.
15.4 Robust and Efficient R Code
R can be a robust, fast and efficient programming language, but some coding practices can be very unfortunate. Here are some suggestions.
15.4.1 Guiding Principles
The primary principle is to make sure your code is correct. Use
identical()
orall.equal()
to ensure correctness, and unit tests to ensure consistent results across code revisions.Write robust code. Avoid efficiencies that do not easily handle edge cases such as 0 length or
NA
values.Know when to stop trying to be efficient. If the code takes a fraction of a second to evaluate, there is no sense in trying for further improvement. Use
system.time()
or a package like microbenchmark to quantify performance gains.
15.4.2 Reuse Existing Functionality and Classes
Emphasized again for good measure! Reuse existing import/export methods, functionality, and classes whenever possible.
15.4.3 Vectorize
Many R operations are performed on the whole object, not just the elements of
the object (e.g., sum(x)
instead of x[1] + x[2] + x[2] + ...
). In
particular, relatively few situations require an explicit for
loop.
Vectorize, rather than iterate (for
loops, lapply()
, apply()
are
common iteration idioms in R). A single call y <- sqrt(x)
with a
vector x
of length n
is an example of a vectorized function. A
call such as y <- sapply(x, sqrt)
or a for
loop for (i in seq_along(x)) y[i] <- sqrt(x[i])
is an iterative version of the same
call, and should be avoided. Often, iterative calculations that can be
vectorized are “hidden” in the body of a for
loop
for (i in seq_len(n)) {
## ...
tmp <- foo(x[i])
y <- tmp + ## ...
}
and can be ‘hoisted’ out of the loop
tmp <- foo(x)
for (i in seq_len(n)) {
## ...
y <- tmp[i] + ##
}
Often this principle can be applied repeatedly, and an iterative
for
loop becomes a few lines of vectorized function calls.
15.4.4 ‘Pre-allocate and fill’ if iterations are necessary
Preallocate-and-fill (usually via lapply()
or vapply()
) rather
than copy-and-append. If creating a vector or list of results, use
lapply()
(to create a list) or vapply()
(to create a vector)
rather than a for
loop. For instance,
n <- 10000
x <- vapply(seq_len(n), function(i) {
## ...
}, integer(1))
manages the memory allocation of x
and compactly represents the
transformation being performed. A for
loop might be appropriate if
the iteration has side effects (e.g., displaying a plot) or where
calculation of one value depends on a previous value. When creating a
vector in a for
loop, always pre-allocate the result
x <- integer(n)
if (n > 0) x[1] <- 0
for (i in seq_len(n - 1)) {
## x[i + 1] <- ...
}
Never adopt a strategy of ‘copy-and-append’
not_this <- function(n) {
x <- integer()
for (i in seq_len(n))
x[i] = i
x
}
This pattern copies the current value of x
each time through the
loop, making n^2 / 2
total copies, and scale very poorly even for
trivial computations:
> system.time(not_this(1000)
user system elapsed
0.004 0.000 0.004
> system.time(not_this(10000))
user system elapsed
0.169 0.000 0.168
> system.time(not_this(100000))
user system elapsed
22.827 1.120 23.936
15.4.5 Avoid 1:n
style iterations
Write seq_len(n)
or seq_along(x)
rather than 1:n
or
1:length(x)
. This protects against the case when n
or length(x)
is 0 (which often occurs as an unexpected ‘edge case’ in real code) or
negative.
15.4.6 Parallel Recommendations
We recommend using BiocParallel. It provides a
consistent interface to the user and supports the major parallel computing
styles: forks and processes on a single computer, ad hoc clusters,
batch schedulers and cloud computing. By default, BiocParallel
chooses a parallel back-end appropriate for the OS and is supported
across Unix, Mac and Windows. Coding requirements for BiocParallel
are:
- Use
lapply()
-style iteration instead of explicit for loops. - The
FUN
argument tobplapply()
must be a self-contained function; all symbols used in the function are from default R packages, from packagesrequire()
’ed in the function, or passed in as arguments. - Allow the user to specify the BiocParallel back-end. Do this by
invoking
bplapply()
without specifyingBPPARAM
; the user can then override the default choice withBiocParallel::register()
.
For more information see the BiocParallel vignette.
When implementing in package development, a minimal number of cores (1 or 2) should be set as a default.