我在shiny里面提了个issues: https://github.com/rstudio/shiny/issues/3767,但是大佬的回答没看懂,不是很明白为什么需要"quote" 表达式,然后使用rlang::inject
这个函数
shiny renderSVG问题请教
我想你链接里提到的“complicated technical reasons”,基本就是Advanced R里的Metaprogramming一章
fenguoerbian Base R里有一些函数和使用实例是体现元编程的吗?还想了解下在什么情况下会用到?楼上各位大佬用中文简单介绍一下?
Cloud2016
Base R的话,例如线性模型的lm
,还有处理数据的subset
, transform
,还有with
等函数都用到non-standard evaluation,所以我觉得都是涉及到元编程的。
我觉得一般来说如果这个函数能够接受用户直接写(数据框中的)变量名,而不需要把这些变量名用引号包起来作为字符串,那其实应该就涉及到NSE,也就涉及到元编程。当然按我这个理解其实$
也是元编程的。
iris$Species
`$`(iris, Species)
甚至在.GlabalEnv
里给Species
赋其它值之后
Species <- "Petal.Width"
`$`(iris, Species)
$
都是取到iris
里同一列。
fenguoerbian 我明白了,原来自己一直在用 NSE。所以, 楼主的问题其实就是 get 函数操作带来 NSE 然后怎么在 tidyverse 里用 NSE,而 tidyverse 有一套全新的用法,这个全新的用法该怎么用的问题,比如提到的 rlang 包和 quote 函数。不知道我理解的对不对?
Liripo 我个人建议楼主以后提问题可以把想要实现的东西说一下,不仅仅贴代码,代码不一定是想要实现的。实现也不一定只有一种方式,我以前写过一些几千行的 Shiny App 大量用到 NSE 不过一点没有用到 tidyverse,但被迫间接导入了不少,一般 Base R(比如 get/mget) + data.table + shiny + DT + 其他(比如 plotly 、leaflet 等)也是一个方案。
Cloud2016 好的,我那里没有说清楚。主要没法理解为什么下面这一段代码在renderSVG函数中使用ggplot的时候运行良好,但是使用plot(1:10)
就不行了。
library(shiny)
renderSVG <- function(expr,id,...) {
session <- get("session",envir = rlang::caller_env())
renderImage({
width <- session$clientData[[sprintf("output_%s_width",id)]]
height <- session$clientData[[sprintf("output_%s_height",id)]]
file <- htmltools::capturePlot(
expr, tempfile(fileext = ".svg"),
svglite::svglite,
width = width/96, height = height/96, ...
)
list(src = file,contentType = 'image/svg+xml')
},deleteFile = FALSE)
}
ui <- fluidPage(
imageOutput("test")
)
server <- function(input, output,session) {
output$test <- renderSVG({
ggplot2::ggplot()
},id = "test")
}
runApp(shinyApp(ui, server))
Liripo
会不会是因为 plot 返回的是 NULL,而 ggplot 返回的是一个可以 print 的对象
正如 meeeeeeeeo 所说,那可不可以理解与 NSE 没有关系呀?ggplot2 和 lattice 都是基于 grid 系统,返回图形对象,并调用 print 方法才能显示图形,而 plot 来自基础作图系统,不带返回对象的。
确实,plot
返回NULL应该是原因。不过我对shiny
交互运行的原理其实并不完全理解。我给renderSVG
加了一些打印,来查看expr
传入的到底是啥
renderSVG <- function(expr,id,...) {
session <- get("session",envir = rlang::caller_env())
renderImage({
width <- session$clientData[[sprintf("output_%s_width",id)]]
height <- session$clientData[[sprintf("output_%s_height",id)]]
print("before capture, the expr is: ")
print(rlang::enexpr(expr))
file <- htmltools::capturePlot(
expr,
tempfile(fileext = ".svg"),
svglite::svglite,
width = width/96, height = height/96, ...
)
print("after capture, the `expr` is :“)
print(rlang::enexpr(expr))
print(file) # this is the filename of image
list(src = file,contentType = 'image/svg+xml')
},
deleteFile = FALSE)
}
可以发现,除了启动时候第一次的expr
是用户传入的表达式,后面的都是expr
运行后的结果。所以如果server
函数里用的ggplot
,那么上面的函数会打印第一次运行的表达式,后续会在rstudio的plots面板里画图。而如果server
函数里用的plot
,那么上面的函数会在第一次时打印表达式,后续在控制台打印的都是NULL,因为plot
返回的是NULL。所以相当于除了启动时运行了一次,后面交互的时候都是直接拿的之前的运行结果?这里是个我不太明白shiny运行原理的地方。
而如果把renderSVG
里面captureplot
那里换成
file <- htmltools::capturePlot(
!!rlang::enexpr(expr),
tempfile(fileext = ".svg"),
svglite::svglite,
width = width/96, height = height/96, ...
)
应该就和链接里给的解答效果一致,可以使得每次传给capturePlot
的都是用户给的表达式。
- 已编辑
fenguoerbian 可以发现,除了启动时候第一次的expr是用户传入的表达式,后面的都是expr运行后的结果。
这不是 shiny 的问题,而是 R 自身的一个特性,就是参数的延迟运行(delayed evaluation),而一旦运行一次,那么原表达式就彻底丢了,只剩下它的值,如果后面还有别的地方要用到这个参数,那么这个参数就不会再被运行一遍,它已经化身为值。这个延迟运行有其解毒办法,就是 Base R 里的 substitute()
/ quote()
那一套(rlang/tidy 那些魔法基本上也是这样)。除了这样少数的特殊解毒魔法,绝大多数 R 函数运行的时候都会遵循我第一句话说的特性。
这个问题困扰用户的情形通常只有一种,就是参数的运行过程有副作用(side effect);所谓副作用,就是代码运行不仅仅是返回值,还带来外部变化,比如打印、写文件、打开画图窗口等等。如果你依赖这些副作用,那么“参数的延迟运行”以及“仅仅运行一次就化身为值”这两个特性就会坑到你。plot()
与 ggplot()
的区别正好在于前者是副作用(画图),副作用只能作用一次,然后返回 NULL,而后者是返回可以重用的值。
我第一次被这个问题困扰到是 2009 年:https://stat.ethz.ch/pipermail/r-help/2009-September/403307.html 当时我发现 9 年前(即 2000 年)Ripley 大人已经解释过(那个链接已失效,不用点了)。
举个简单例子:
f = function(x) {
x # 运行,副作用打印消息
print(x) # 只剩下 x 的值,副作用消失
print(substitute(x)) # 通灵
eval(substitute(x)) # 秽土转生,诈尸重新跑起来
}
f(message('hello'))
刚意识到上面没有解释延迟运行的延迟是什么意思:延迟就是(参数)若没用到,就不运行。举两个简单例子,可能会让其它语言的用户感到吃惊:
f = function(x) {
message('Hello!')
}
f(stop('No!'))
f()
函数中并没用到 x
参数,所以就算传入一个报错给 x
,这个报错也不会报出来。
这个延迟是重度拖延症的延迟,就算嵌套函数调用也是如此:
g = function(y) {
f(y)
}
g(stop('No!'))
把报错传给 g()
的 y
参数,内部再进一步传给 f()
,但 f()
没用它的参数,所以这个报错还是无法起爆。
总结:参数没用到的时候,永远支棱;一旦用到,立马坍塌。
yihui 延迟运行的概念我一直知道,但是直到看了你后面的例子才发现我对这个概念的理解其实并不深,对于楼主这里的情况也没有反应得过来。