I’ve recently spent some time at work updating a few R packages we’ve built and deployed over the last several years, and during these updates I’ve run up against an old foe:
my_package::run_model(...) #> Error in UseMethod("predict") : #> no applicable method for 'predict' applied to an object of class "ranger"
In case you’re unfamiliar, this error stems from R’s S3 method-dispatch system:
model is a “ranger” object, so R goes looking for a “ranger” version of the ‘predict’ method, but can’t find it—even though I have the ranger package installed and
Imported. I’ve seen this occasionally ever since I stated working with R in 2016, but until now I’ve treated the symptoms. I usually patch around this error by forcing R to use the (private) function
ranger:::predict.ranger() even though this is poor form, shouldn’t need to be done, and raises
NOTEs during the
R CMD check process.
I recently made some progress, though, by realizing that things ‘magically’ worked if I called, e.g.,
library(ranger) at the top of whatever script uses my package to get things done. And then, finally, I realized something else. One embedded call would work A-OK:
result <- ranger::predictions(predict(model, data))
while a separate call in another function would not:
result <- predict(model, data)
These two clues unlocked the solution.1 It turns out that, while
Importing a package is necessary, it is not sufficient in order for its S3 methods to be made available during runtime. This includes methods like
predict.<class>(). In fact, S3 methods aren’t registered at all unless you tell R to use some bit of the imported package earlier in your program.2
You can test this by calling
methods(predict), which, in my example, will not list
predict.ranger(). After calling any ranger function, however—say,
ranger::predictions()—a subsequent call to
methods(predict) does indeed list
predict.ranger() as an available S3 method. This is why the ‘predict’ call wrapped in
ranger::predictions worked for years, and I didn’t even notice; the
:: call causes R to immediately load
ranger along with its various S3 methods, so
predict() dispatches just fine.
If the S3 method were exported from a package, I suppose one could simply import the
predict.<class>() method directly, e.g., in roxygen2 documentation syntax:
#' @importFrom <package name> <method name>
But this doesn’t work if the S3 method isn’t exported—and a ‘predict’ method shouldn’t really need to be.
With the problem better understood, my solution is to do one of two things:
- Call, or import, another function from the package (e.g.,
ranger::predictions()). This alone should cause the package to attach its namespace and register S3 methods when my package is loaded.
- Add a single
requireNamespace(<package name>, quietly = TRUE)call to the top of the function of interest, or to my package’s
library(), this causes R to register the appropriate S3 methods, etc., but prevents the package from “attaching”, from adding itself to the search path so that all its functions are globally available.3 You can confirm this again by checking
methods(predict)before and after calling ‘requireNamespace’, including for non-exported S3 methods like
Live and learn, I guess. 🤷🏻♂️