rstudio / plumber

Turn your R code into a web API.
https://www.rplumber.io
Other
1.39k stars 256 forks source link

Add private-arg and values from the global environment for endpoint definition #338

Closed colearendt closed 5 years ago

colearendt commented 5 years ago

After some discussion with @blairj09 , I am a bit ambivalent on this, but wanted to surface the idea for discussion. In plumber, I tend to occasionally create functions that are usable elsewhere and name them as a result. However, when using a database or other global value, we have no real choice but to break the functional paradigm and rely on an unstated global object. I think a solution would be desirable. Ideally one that allows suppressing an argument from being accessible to the client, as well as defining the value from the global environment.

Today:

my_conn <- dbConnect()
#* @get 
my_endpoint <- function() {
  dbGetQuery(con, "SELECT 1")
}

Desired. Something to the effect of:

my_conn <- dbConnect()
#* @get
#* @private-arg con = my_conn
my_endpoint <- function(con) {
  dbGetQuery(con, "SELECT 1")
}

Discussion points:

schloerke commented 5 years ago

I understand the pain point and wanting to fix it. I believe we are fighting against R and the current design setup of sourcing the file and reading functions under plumber comment blocks.

However, since we can do full R code sourcing, we can use environment encapsulation.

#* @get /
my_endpoint <- local({
  my_conn <- dbConnect()
  function(con) {
    dbGetQuery(con, "SELECT 1")
  }
})

Example...

a <- local({
  my_val <- 4
  function(...) {
    my_val
  }
})
ls()
#> [1] "a"
a()
#> [1] 4

Created on 2018-11-13 by the reprex package (v0.2.1)

This doesn't work the best with a single connection object.


Similar to pryr::partial, we could make a function that doesn't expose the variable, but calls function f with the variables.

add <- function(a, b) { a + b }
partial_add <- pryr::partial(add, b = 3)
partial_add
#> function (...) 
#> add(b = 3, ...)
partial_add(4) # 7
#> [1] 7

We could make it so that it would be something like below, where instead of ..., we pass over the formals so that plumber picks up which args are allowed to be sent in.

my_add <- with_private_args(`+`, e2 = 3)
my_add
#> function(e1) {
#>   fn(e1, e2 = 3)
#> }

We could also encourage function wrapping manually to keep the cleanliness of plumber and usefulness of 'good' code.

Example:

#' Public Documentation
#' @export
my_fn <- function(con) {
  dbGetQuery(con, "SELECT 1")
}

#* @get /equiv_example_1
function() {
  my_conn <- dbConnect()
  my_fn(conn)
}

#* @get /equiv_example_2
with_private_args(my_fn, dbConnect())

@colearendt, @blairj09 I think the cleanest solution going forward is to make a with_private_args like function.

When wanting two separate function behaviors, I'd prefer to have two separate blocks. While the idea of a #* @private-arg con = my_conn addresses keeping everything in one location, it gets messy real quick. I'd prefer to have items defined globally so the user can inspect what function is actually being used.

schloerke commented 5 years ago

Revisiting this again, I'm leaning back towards the initial setup of defining objects within the same file and created functions that are aware of the objects created before. Ex:

my_conn <- dbConnect()

#* @get /equiv_example_1
function() {
  my_fn(my_conn)
}

@colearendt, I believe you're trying to combine a function api with a different plumber api in the same step. Would you feel more at ease if there was a with_private_args function to make sure the variable existed?

To answer your earlier question about naming functions:

schloerke commented 5 years ago

@jcheng5 Thoughts?

colearendt commented 5 years ago

@schloerke Sorry for the late response here. I do kinda like the with_private_args idea, so there is a way to ensure the object exists in "pure" functional practice if that is desirable. I can also see how the comments in #* blocks would get ugly super quick. It took me a while in my Shiny / Plumber app building to realize that I was both loving functional programming and abusing functional programming at the same time 😆