Search code examples
rmethodsr-s3

Partial matching confusion when arguments passed through dots ('...')


I've been working on an R package that is just a REST API wrapper for a graph database. I have a function createNode that returns an object with class node and entity:

# Connect to the db.
graph = startGraph("http://localhost:7474/db/data/")

# Create two nodes in the db.
alice = createNode(graph, name = "Alice")
bob = createNode(graph, name = "Bob")

> class(alice)
[1] "node"   "entity"
> class(bob)
[1] "node"   "entity"

I have another function, createRel, that creates a relationship between two nodes in the database. It is specified as follows:

createRel = function(fromNode, type, toNode, ...) {
  UseMethod("createRel")
}

createRel.default = function(fromNode, ...) {
  stop("Invalid object. Must supply node object.")
}

createRel.node = function(fromNode, type, toNode, ...) {
  params = list(...)

  # Check if toNode is a node.
  stopifnot("node" %in% class(toNode))

  # Making REST API calls through RCurl and stuff.
}

The ... allows the user to add an arbitrary amount of properties to the relationship in the form key = value. For example,

rel = createRel(alice, "KNOWS", bob, since = 2000, through = "Work")

This creates an (Alice)-[KNOWS]->(Bob) relationship in the db, with the properties since and through and their respective values. However, if a user specifies properties with keys from or to in the ... argument, R gets confused about the classes of fromNode and toNode.

Specifying a property with key from creates confusion about the class of fromNode. It is using createRel.default:

> createRel(alice, "KNOWS", bob, from = "Work")

Error in createRel.default(alice, "KNOWS", bob, from = "Work") : 
Invalid object. Must supply node object. 
3 stop("Invalid object. Must supply node object.") 
2 createRel.default(alice, "KNOWS", bob, from = "Work") 
1 createRel(alice, "KNOWS", bob, from = "Work")

Similarly, if a user specifies a property with key to, there is confusion about the class of toNode, and stops at the stopifnot():

Error: "node" %in% class(toNode) is not TRUE 
4 stop(sprintf(ngettext(length(r), "%s is not TRUE", "%s are not all TRUE"), 
  ch), call. = FALSE, domain = NA) 
3 stopifnot("node" %in% class(toNode)) 
2 createRel.node(alice, "KNOWS", bob, to = "Something") 
1 createRel(alice, "KNOWS", bob, to = "Something")

I've found that explicitly setting the parameters in createRel works fine:

rel = createRel(fromNode = alice,
                type = "KNOWS",
                toNode = bob,
                from = "Work",
                to = "Something")
# OK

But I am wondering how I need to edit my createRel function so that the following syntax will work without error:

rel = createRel(alice, "KNOWS", bob, from = "Work", to = "Something")
# Errors galore.

The GitHub user who opened the issue mentioned it is most likely a conflict with setAs on dispatch, which has arguments called from and to. One solution is to get rid of ... and change createRel to the following:

createRel = function(fromNode, type, toNode, params = list()) {
  UseMethod("createRel")
}

createRel.default = function(fromNode, ...) {
  stop("Invalid object. Must supply node object.")
}

createRel.node = function(fromNode, type, toNode, params = list()) {
  # Check if toNode is a node.
  stopifnot("node" %in% class(toNode))

  # Making REST API calls through RCurl and stuff.
}

But, I wanted to see if I had any other options before making this change.


Solution

  • Not really an answer, but...

    The problem is that the user-provided argument 'from' is being (partially) matched to the formal argument 'fromNode'.

    f = function(fromNode, ...) fromNode
    f(1, from=2)
    ## [1] 2
    

    The rules are outlined in section 4.3.2 of RShowDoc('R-lang'), where named arguments are exact matched, then partial matched, and then unnamed arguments are assigned by position.

    It's hard to know how to enforce exact matching, other than using single-letter argument names! Actually, for a generic this might not be as trite as it sounds -- x is a pretty generic variable name. If 'from' and 'to' were common arguments to ... you could change the argument list to "fromNode, , ..., from, to", check for missing(from) in the body of the function, and act accordingly; I don't think this would be pleasant, and the user would invariable provide an argument 'fro'.

    While enforcing exact matching (and errors, via warn=2) by setting global options() might be helpful in debugging (though by then you'd probably know what you were looking for!) it doesn't help the package author who is trying to write code to work for users in general.

    It might be reasonable to ask on the R-devel mailing list whether it might be time for this behavior to be changed (on the 'several releases' time scale); partial matching probably dates as a 'convenience' from the days before tab completion.