Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle S3 #959

Closed
2 tasks done
wlandau opened this issue Jul 28, 2019 · 1 comment
Closed
2 tasks done

Handle S3 #959

wlandau opened this issue Jul 28, 2019 · 1 comment

Comments

@wlandau
Copy link
Member

wlandau commented Jul 28, 2019

Prework

Description

S3 methods should be dependencies of their respective generics. We can enforce this in analyze_function().

drake/R/analyze_code.R

Lines 173 to 182 in b551da2

analyze_function <- function(expr, results, locals, allowed_globals) {
expr <- unwrap_function(expr)
if (typeof(expr) != "closure") {
return()
}
locals <- ht_clone(locals)
ht_set(locals, names(formals(expr)))
ignore(walk_code)(formals(expr), results, locals, allowed_globals)
ignore(walk_code)(body(expr), results, locals, allowed_globals)
}

If we detect UseMethod() in the function, we just need to find all the matching S3 methods in allowed_globals.

@wlandau wlandau changed the title Handle S3 Handle custom S3 Jul 28, 2019
@wlandau wlandau changed the title Handle custom S3 Handle S3 Jul 28, 2019
@wlandau
Copy link
Member Author

wlandau commented Jul 28, 2019

Now implemented. This does change the dependency structure of some workflows, but not many. It only affects people who define their own S3 generics. drake does not scan un-namespaced package functions, and that is not going to change, so we cannot dive into un-namespace predefined generics in the general case.

How it works

If the generic do_stuff() and the method stuff.your_class() are defined in envir, and if do_stuff() has a call to UseMethod("stuff"), then drake's code analysis will detect stuff.your_class() as a dependency of do_stuff().

do_stuff <- function(x, ...) {
  UseMethod("stuff")
}

stuff.class1 <- function(x, ...) {
  sqrt(x)
}

stuff.class2 <- function(x, ...) {
  x ^ 2
}

library(drake)
plan <- drake_plan(x = do_stuff(make_stuff()))
config <- drake_config(plan)
vis_drake_graph(config)

Created on 2019-07-28 by the reprex package (v0.3.0)

So in terms of triggering rebuilds in a drake workflow, this behavior effectively condenses a user-defined generic and all its user-defined S3 methods into a single function. E.g. do_stuff becomes this:

do_stuff <- function(x, ...) {
  if (inherits(x, "class1")) {
    sqrt(x)
  } else if (inherits(x, "class2")) {
    x ^ 2
  } else {
    stop(
      "no applicable method for 'do_stuff' ",
      "applied to an object of class ",
      class(x)
    )
  }
}

The solution is not perfect. The drake_plan() command do_stuff(object_of_class1) will rebuild whenever stuff.class2() changes. However, it is the best we can do because static code analysis does not tell us the class of object_of_class1 in advance. I believe this is still better than not accounting for S3 at all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant