Skip to content

pluck(x, NULL) errs; use cases for why it shouldn't? #813

@mmuurr

Description

@mmuurr

This is tangentially-related to #480, but is manifested in a slightly different way, so I figured to open a new issue. Currently both of these throw errors:

pluck(x, NULL)
pluck(x, character(0))

I think this might be too harsh.

In #480, @jennybc mentions:

It does seem sort of troubling that pluck() won't allow you to retain, say, character(0) vs NULL inside some workflow. Yet I don't have any current compelling example where this is harmful.

Workflow Example 1

One such use case (where NULL and/or character(0) are part of a workflow) is in Shiny apps. shiny::radioButtons, e.g., can be configured to have no default selection, in which case the reactive value server-side is NULL. If those radio buttons are used to pluck a dataset out of a list, the app author now needs to add additional code (admittedly, perhaps not a lot of additional code by using shiny::req) to check for the un-set case. Adding this additional code for some app improvement is great; needing to add additional code to prevent a pluck error feels less great, especially since pluck already has .default semantics. In the app case, for example, one might want to set .default to hard-coded example dataset for the rest of the app to use on initialization:

output$some_output <- renderSomething({
  pluck(my_data, input$my_radio_buttons, .default = my_default_data_set) %>%
    downstream_processing()
})

^ That fails when input$my_radio_buttons is NULL. Adding additional req or isTruthy (or equivalent) logic simply to avoid the pluck error when .default is present seems cumbersome.

Workflow Example 2

Another workflow example is simply using pluck in one list to retrieve a value that is then used as a key when pluck-ing from another list:

obj1 <- list(my_type = "type1", some = "other", data = "here")
obj2 <- list(
  foo = list(  ## just for the sake of inserting a nested list to demonstrate pluck's utility
    "type1" = c(some, stuff, here),
    "type2" = c(some, other, stuff, here)
  )
)
## let's retrieve my_type from obj1 and use that to fetch stuff from obj2:
pluck(obj2, "foo", pluck(obj1, "my_type"), .default = c(some, default, stuff))

^ That works great when "my_type" is present in obj1.
But if "my_type" is missing, the outer pluck throws an error because pluck(obj1, "my_type") resolves to NULL. So we have to add conditionals to short-circuit the outer pluck call, returning the .default in that case and keeping the .default in the outer pluck in the instances where "my_type" is present in obj1 but doesn't have a corresponding value in obj2.

In both such examples, there're clearly ways around this, mostly with lots of conditionals (or wrapping pluck in tryCatch), but the .default option in pluck feels like it was introduced to handle non-existent keys, and NULL feels like a non-existent key. Probably the same reasoning holds for character(0) (and other zero-length values passed as keys), but I can definitely see throwing errors when passed a length > 1 vector as a pluck element key. Likewise, chuck() should throw an error when given NULL as a key, as it's advertised as being strict.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions