在R使用局部变量defmacro(defmacro that uses local variable

2019-10-23 05:41发布

下面是从代码http://cran.r-project.org/doc/Rnews/Rnews_2001-3.pdf :

defmacro <- function(..., expr){
    expr <- substitute(expr)
    a <- substitute(list(...))[-1]
    ## process the argument list
    nn <- names(a)
    if (is.null(nn)) nn <- rep("", length(a))
    nn
    for(i in seq(length=length(a))) {
        if (nn[i] == "") {
            nn[i] <- paste(a[[i]])
            msg <- paste(a[[i]], "not supplied")
            a[[i]] <- substitute(stop(foo),
                    list(foo = msg))
            print(a)
        }
    }
    names(a) = nn
    a = as.list(a)
    ff = eval(substitute( 
                    function() { 
                        tmp = substitute(body)
#                       # new environment to eval expr
#                       private_env = new.env()
#                       pf = parent.frame()
#                       for(arg_name in names(a)) {
#                           private_env[[a]] = pf[[a]]
#                       }
#                       eval(tmp, private_env)
                        eval(tmp, parent.frame())
                    }, 
                    list(body = expr)))
    formals(ff) = a
    mm = match.call()
    mm$expr = NULL
    mm[[1]] = as.name("macro")
    mm_src = c(deparse(mm), deparse(expr))
    attr(ff, "source") = mm_src
    ff
}
setna = defmacro(a, b, values, expr = {a$b[a$b %in% values] = NA; a})
dat = data.frame(x = 1:4, y = rep(-9, 4))
setna(dat, y, -9)
dat

笔者鼓励读者想出了使用本地变量,而不是EVAL在父帧(这可能是危险的,因为它可以在父框架修改对象)的新defmacro。

我试图创建一个新的环境,并从父环境复制变量,和eval函数体有(代码注释掉),但结果是,它不EVAL身体都没有。

谁能帮助吗?

@bergant暗示eval(tmp, new.env())就行了,实际上它的工作原理时,宏不能嵌套的,但在这里我们有一个问题:

#' TODO: doc
#' @export 
defmacro <- function(..., expr){
    expr <- substitute(expr)
    a <- substitute(list(...))[-1]
    ## process the argument list
    nn <- names(a)
    if (is.null(nn)) nn <- rep("", length(a))
    nn
    for(i in seq(length=length(a))) {
        if (nn[i] == "") {
            nn[i] <- paste(a[[i]])
            msg <- paste(a[[i]], "not supplied")
            a[[i]] <- substitute(stop(foo),
                    list(foo = msg))
            print(a)
        }
    }
    names(a) = nn
    a = as.list(a)
    ff = eval(substitute( 
                    function() { 
                        tmp = substitute(body)
                        eval(tmp, parent.frame())
                    }, 
                    list(body = expr)))
    formals(ff) = a
    mm = match.call()
    mm$expr = NULL
    mm[[1]] = as.name("macro")
    mm_src = c(deparse(mm), deparse(expr))
    attr(ff, "source") = mm_src
    ff
}


#' IfLen macro
#' 
#' Check whether a object has non-zero length, and 
#' eval expression accordingly.
#' 
#' @param df An object which can be passed to \code{length}
#' @param body1 If \code{length(df)} is not zero, then this clause is evaluated, otherwise, body2 is evaluated.
#' @param body2 See above.
#' 
#' @examples 
#' ifLen(c(1, 2), { print('yes!') }, {print("no!")})
#' 
#' @author kaiyin
#' @export
ifLen = defmacro(df, body1, body2 = {}, expr = {
            if(length(df) != 0) {
                body1
            } else {
                body2
            }
        })

#' IfLet macro
#' 
#' Eval expression x, assign it to a variable, and if that is TRUE, continue
#' to eval expression1, otherwise eval expression2. Inspired by the clojure 
#' \code{if-let} macro.
#' 
#' @param sym_str a string that will be converted to a symbol to hold value of \code{x}
#' @param x the predicate to be evalueated, and to be assigned to a temporary variable as described in \code{sym_str}
#' @param body1 expression to be evaluated when the temporary variable is TRUE.
#' @param body2 expression to be evaluated when the temporary variable is FALSE.
#' 
#' @examples 
#' ifLet(..temp.., TRUE, {print(paste("true.", as.character(..temp..)))}, 
#'      {print(paste("false.", as.character(..temp..)))})
#' ifLet("..temp..", TRUE, {print(paste("true.", as.character(..temp..)))}, 
#'      {print(paste("false.", as.character(..temp..)))})
#' 
#' @author kaiyin
#' @export
ifLet = defmacro(sym_str, x, body1, body2={}, expr = {
            stopifnot(is.character(sym_str))
            stopifnot(length(sym_str) == 1)
            assign(sym_str, x)
            if(eval(as.symbol(sym_str))) {
                body1
            } else {
                body2
            }
        })

#
#setMethod("ifLet",
#       signature(sym = "character", x = "ANY", body1 = "ANY", body2 = "ANY"),
#       function(sym, x, body1, body2 = {}) {
#           e = new.env()
#           sym_str = deparse(substitute(sym))
#           ifLet(sym_str, x, body1, body2)
#       })
#
##' TODO: doc
##' @export
#setMethod("ifLet",
#       signature(sym = "character", x = "ANY", body1 = "ANY", body2 = "ANY"),
#       function(sym, x, body1, body2 = {}) {
#           stopifnot(length(sym) == 1)
#           e = new.env()
#           assign(sym, x, envir = e)
#           if(e[[sym]]) {
#               eval(substitute(body1), e, parent.frame())
#           } else {
#               eval(substitute(body2), e, parent.frame())
#           }
#       })










#' IfLetLen macro
#' 
#' Similar to ifLet, but conditioned on whether the length of 
#' the result of \code{eval(x)} is 0.
#' 
#' 
#' @param x the predicate to be evalueated, and to be assigned to a temporary var called \code{..temp..}
#' @param body1 expression to be evaluated when \code{..temp..} is TRUE.
#' @param body2 expression to be evaluated when \code{..temp..} is FALSE.
#' 
#' @examples 
#' ifLetLen("..temp..", 1:3, {print(paste("true.", as.character(..temp..)))}, 
#'      {print(paste("false.", as.character(..temp..)))})
#' 
#' @author kaiyin
#' @export
ifLetLen = defmacro(sym_str, x, body1, body2={}, expr = {
            stopifnot(is.character(sym_str))
            stopifnot(length(sym_str) == 1)
            assign(sym_str, x)
            ifLen(eval(as.symbol(sym_str)), {
                body1
            }, {
                body2
            })
        })

如果你运行这个测试:

ifLetLen("..temp..", 1:3, {print(paste("true.", as.character(..temp..)))}, 
        {print(paste("false.", as.character(..temp..)))})

你会得到一个object not found error

Answer 1:

你可以添加环境的属性defmacro

defmacro <- function(..., expr, env = parent.frame()){
  expr <- substitute(expr)
  a <- substitute(list(...))[-1]
  ## process the argument list
  nn <- names(a)
  if (is.null(nn)) nn <- rep("", length(a))
  nn
  for(i in seq(length=length(a))) {
    if (nn[i] == "") {
      nn[i] <- paste(a[[i]])
      msg <- paste(a[[i]], "not supplied")
      a[[i]] <- substitute(stop(foo),
                           list(foo = msg))
      print(a)
    }
  }
  names(a) = nn
  a = as.list(a)
  ff = eval(substitute( 
    function() { 
      tmp = substitute(body)
      eval(tmp, env)
    }, 
    list(body = expr)))
  formals(ff) = a
  mm = match.call()
  mm$expr = NULL
  mm[[1]] = as.name("macro")
  mm_src = c(deparse(mm), deparse(expr))
  attr(ff, "source") = mm_src
  ff
}

这里我们使用new.env

ifLen = defmacro(df, body1, body2 = {}, expr = {
  if(length(df) != 0) {
    body1
  } else {
    body2
  }
}, env = new.env())

但在这里我们是不是:

ifLetLen = defmacro(sym_str, x, body1, body2={}, expr = {
  stopifnot(is.character(sym_str))
  stopifnot(length(sym_str) == 1)
  assign(sym_str, x)
  ifLen(eval(as.symbol(sym_str)), {
    body1
  }, {
    body2
  })
})

ifLetLen("..temp..", 1:3, {print(paste("true.", as.character(..temp..)))}, 
     {print(paste("false.", as.character(..temp..))); xxx <- 69})

# [1] "true. 1" "true. 2" "true. 3"

第一个例子:

setna = defmacro(a, b, values, expr = {a$b[a$b %in% values] = NA; a}, env = new.env())
dat = data.frame(x = 1:4, y = rep(-9, 4))

> setna(dat, y, -9)
#   x  y
# 1 1 NA
# 2 2 NA
# 3 3 NA
# 4 4 NA
> dat
#   x  y
# 1 1 -9
# 2 2 -9
# 3 3 -9
# 4 4 -9

提出的方案的问题是,你必须要对环境的关心(什么是可见的,其中表现评估哪些功能)。 我不觉得作为一个编程工具,它非常透明。

注意:它不会(从原来的纸)解决局部变量的问题 - 它只是把在不同的环境中的一切(如典型的R里面的函数做无论如何)。



文章来源: defmacro that uses local variables in R
标签: r macros