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))

          正如 meeeeeeeeo 所说,那可不可以理解与 NSE 没有关系呀?ggplot2 和 lattice 都是基于 grid 系统,返回图形对象,并调用 print 方法才能显示图形,而 plot 来自基础作图系统,不带返回对象的。

            meeeeeeeeo

            Cloud2016

            确实,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 延迟运行的概念我一直知道,但是直到看了你后面的例子才发现我对这个概念的理解其实并不深,对于楼主这里的情况也没有反应得过来。