Программируемая версия подмножества - для оценки ее состояния при вызове из другой функции

Как указано subset():

Предупреждение. Это удобная функция, предназначенная для интерактивного использования.

Я узнал из эту замечательную статью не только секрет этого предупреждения, но и хорошее понимание substitute(), match.call(), eval(), quote(), call, promise и другие связанные с ним объекты R, которые немного сложны.

Теперь я понимаю, для чего это предупреждение. Суперпростая реализация subset() может быть следующей:

subset = function(x, condition) x[eval(substitute(condition), envir=x),]

Пока subset(mtcars, cyl==4) возвращает таблицу строк в mtcars, удовлетворяющую cyl==4, обтекание subset() в другой функции не выполняется:

sub = function(x, condition) subset(x, condition)

sub(mtcars, cyl == 4)
# Error in eval(expr, envir, enclos) : object 'cyl' not found

Использование исходной версии subset() также дает точно такое же условие ошибки. Это связано с ограничением пары substitute()-eval(): она отлично работает, а condition - cyl==4, но когда condition передается через огибающую функцию sub(), аргумент condition subset() будет больше не cyl==4, а вложенный condition в тело sub(), а eval() - неудачно - это немного сложно.

Но существует ли какая-либо другая реализация subset() с точно такими же аргументами, которая была бы безопасна в программировании, то есть могла бы оценить ее состояние, пока она вызывалась другой функцией?

Ответ 1

Просто потому, что это увлекательное развлечение (??), вот немного другое решение, которое решает проблему, о которой Хэдли указал в комментариях к моему принятому решению.

Hadley опубликовал суть, демонстрируя ситуацию, в которой моя принятая функция идет вразрез. Твист в этом примере (скопирован ниже) состоит в том, что символ, переданный в SUBSET(), определяется в теле (а не в аргументах) одной из вызывающих функций; он становится захваченным substitute() вместо предполагаемой глобальной переменной. Я знаю, что это путают.

f <- function() {
  cyl <- 4
  g()
}

g <- function() {
  SUBSET(mtcars, cyl == 4)$cyl
}
f()

Вот лучшая функция, которая будет заменять только значения символов, найденных в списках аргументов вызывающих функций. Он работает во всех ситуациях, которые до сих пор предлагал Хэдли или я.

SUBSET <- function(`_dat`, expr) {
   ff <- sys.frames()
   n <- length(ff)
   ex <- substitute(expr)
   ii <- seq_len(n)
   for(i in ii) {
       ## 'which' is the frame number, and 'n' is # of frames to go back.
       margs <- as.list(match.call(definition = sys.function(n - i),
                                   call = sys.call(sys.parent(i))))[-1]
       ex <- eval(substitute(substitute(x, env = ll),
                             env = list(x = ex, ll = margs)))
   }
   `_dat`[eval(ex, envir = `_dat`),]
}

## Works in Hadley counterexample ...
f()
# [1] 4 4 4 4 4 4 4 4 4 4 4

## ... and in my original test cases.
sub <- function(x, condition) SUBSET(x, condition)
sub2 <- function(AA, BB) sub(AA, BB)

a <- SUBSET(mtcars, cyl == 4)  ## Direct call to SUBSET()
b <- sub(mtcars, cyl == 4)     ## SUBSET() called one level down
c <- sub2(mtcars, cyl == 4)
all(identical(a, b), identical(b, c))
# [1] TRUE

ВАЖНО: Обратите внимание, что это все еще не является (и не может быть сделано) полезной функцией. Просто нет возможности для функции знать, какие символы вы хотите использовать во всех подстановках, которые она выполняет при работе над стеком вызовов. Существует множество ситуаций, когда пользователи хотели бы, чтобы они использовали значения символов, назначаемых внутри тел функции, но эта функция всегда будет игнорировать их.

Ответ 2

[функция - это то, что вы ищете.? "[". mtcars[mtcars$cyl == 4,] эквивалентен команде подмножества и безопасен для программирования.

sub = function(x, condition) {
 x[condition,]
}

sub(mtcars, mtcars$cyl==4)

Делает то, что вы просите без неявного with() в вызове функции. Специфика сложна, однако такая функция, как:

sub = function(x, quoted_condition) {
  x[with(x, eval(parse(text=quoted_condition))),]
}

sub(mtcars, 'cyl==4')

Сорта делает то, что вы ищете, но есть крайние случаи, когда это приведет к неожиданным результатам.


с помощью data.table и [ функции подмножества вы можете получить неявный with(...), который вы ищете.

library(data.table)
MT = data.table(mtcars)

MT[cyl==4]

есть лучшие, более быстрые способы сделать это подмножество в data.table, но это хорошо иллюстрирует точку.


с помощью data.table вы также можете построить выражения, которые будут оцениваться позже

cond = expression(cyl==4)

MT[eval(cond)]

эти два теперь могут быть переданы через функции:

wrapper = function(DT, condition) {
  DT[eval(condition)]
}

Ответ 3

Здесь альтернативная версия subset(), которая продолжает работать даже тогда, когда она вложена - по крайней мере, пока выражение логической подмножества (например, cyl == 4) будет отправлено на вызов функции верхнего уровня.

Он работает, поднимаясь вверх по стеку вызовов, substitute() на каждом шаге, чтобы в конечном счете захватить выражение логического подмножества, переданное пользователем. При вызове sub2() ниже, например, цикл for обрабатывает стек вызовов от expr до x до AA и, наконец, до cyl ==4.

SUBSET <- function(`_dat`, expr) {
    ff <- sys.frames()
    ex <- substitute(expr)
    ii <- rev(seq_along(ff))
    for(i in ii) {
        ex <- eval(substitute(substitute(x, env=sys.frames()[[n]]),
                              env = list(x = ex, n=i)))
    }
    `_dat`[eval(ex, envir = `_dat`),]
}

## Define test functions that nest SUBSET() more and more deeply
sub <- function(x, condition) SUBSET(x, condition)
sub2 <- function(AA, BB) sub(AA, BB)

## Show that it works, at least when the top-level function call
## contains the logical subsetting expression
a <- SUBSET(mtcars, cyl == 4)  ## Direct call to SUBSET()
b <- sub(mtcars, cyl == 4)     ## SUBSET() called one level down
c <- sub2(mtcars, cyl == 4)    ## SUBSET() called two levels down

identical(a,b)
# [1] TRUE
> identical(a,c)
# [1] TRUE
a[1:5,]
#                 mpg cyl  disp  hp drat    wt  qsec vs am gear carb
# Datsun 710     22.8   4 108.0  93 3.85 2.320 18.61  1  1    4    1
# Merc 240D      24.4   4 146.7  62 3.69 3.190 20.00  1  0    4    2
# Merc 230       22.8   4 140.8  95 3.92 3.150 22.90  1  0    4    2
# Fiat 128       32.4   4  78.7  66 4.08 2.200 19.47  1  1    4    1
# Honda Civic    30.4   4  75.7  52 4.93 1.615 18.52  1  1    4    2

** Для некоторого объяснения конструкции внутри цикла for см. Раздел 6.2, пункт 6 руководства по определению языка R.

Ответ 4

Update:

Вот новая версия, которая устраняет две проблемы:

a) предыдущая версия просто прошла sys.frames() назад. Эта версия следует parent.frames(), пока не достигнет .GlobalEnv. Это важно, например, в subscramble, где кадр scramble следует игнорировать.

b) Эта версия имеет один substitute за уровень. Это предотвращает второй вызов substitute от замены символов с одного уровня выше, которые были введены первым вызовом substitute.

subset <- function(x, condition) {

    call <- substitute(condition)
    frames <- sys.frames()
    parents <- sys.parents()

    # starting one frame up, keep climbing until we get to .GlobalEnv 
    i <- tail(parents, 1)
    while(i != 0) {

        f <- sys.frames()[[i]]

        # copy x into f, except for variable with conflicting names.
        xnames <- setdiff(ls(x), ls(f))
        for (n in xnames) assign(n, x[[n]], envir=f)

        call <- eval(substitute(substitute(expr, f), list(expr=call)))

        # leave f the way we found it
        rm(list=xnames, envir=f)

        i <- parents[i]
    }

    r <- eval(call, x, .GlobalEnv)

    x[r, ]
}

Эта версия пропускает тест @hadley из комментариев:

mtcars $ condition <- 4; subscramble(mtcars, cyl == 4)

К сожалению, следующие два примера ведут себя по-другому:

cyl <- 6; subset(mtcars, cyl==4)
local({cyl <- 6; subset(mtcars, cyl==4)})

Это небольшая модификация первой функции Джоша. В каждом кадре в стеке мы заменяем из x перед подстановкой из фрейма. Это означает, что символы в кадре данных имеют приоритет на каждом шаге. Мы можем избежать псевдо-gensyms, таких как _dat, пропуская кадр subset в цикле for.

subset <- function(x, condition) {

    call <- substitute(condition)
    frames <- rev(sys.frames())[-1]

    for(f in frames) {

        call <- eval(substitute(substitute(expr, x), list(expr=call)))
        call <- eval(substitute(substitute(expr, f), list(expr=call)))
    }

    r <- eval(call, x, .GlobalEnv)

    x[r, ]
}

Эта версия работает в простом случае (стоит проверить, что у нас не было регрессии):

subset(mtcars, cyl == 4)
#                 mpg cyl  disp  hp drat    wt  qsec vs am gear carb
# Datsun 710     22.8   4 108.0  93 3.85 2.320 18.61  1  1    4    1
# Merc 240D      24.4   4 146.7  62 3.69 3.190 20.00  1  0    4    2
# Merc 230       22.8   4 140.8  95 3.92 3.150 22.90  1  0    4    2
# Fiat 128       32.4   4  78.7  66 4.08 2.200 19.47  1  1    4    1
# Honda Civic    30.4   4  75.7  52 4.93 1.615 18.52  1  1    4    2
# Toyota Corolla 33.9   4  71.1  65 4.22 1.835 19.90  1  1    4    1
# Toyota Corona  21.5   4 120.1  97 3.70 2.465 20.01  1  0    3    1
# Fiat X1-9      27.3   4  79.0  66 4.08 1.935 18.90  1  1    4    1
# Porsche 914-2  26.0   4 120.3  91 4.43 2.140 16.70  0  1    5    2
# Lotus Europa   30.4   4  95.1 113 3.77 1.513 16.90  1  1    5    2
# Volvo 142E     21.4   4 121.0 109 4.11 2.780 18.60  1  1    4    2

Он также работает с subscramble и f:

scramble <- function(x) x[sample(nrow(x)), ]
subscramble <- function(x, condition) scramble(subset(x, condition))

subscramble(mtcars, cyl == 4) $ cyl
# [1] 4 4 4 4 4 4 4 4 4 4 4

f <- function() {cyl <- 4; g()}
g <- function() subset(mtcars, cyl == 4) $ cyl

g()
# [1] 4 4 4 4 4 4 4 4 4 4 4

И даже работает в более сложных ситуациях:

gear5 <- function(z, condition) {

    x <- 5
    subset(z, condition & (gear == x))
}

x <- 4
gear5(mtcars, cyl == x)
#                mpg cyl  disp  hp drat    wt qsec vs am gear carb
# Porsche 914-2 26.0   4 120.3  91 4.43 2.140 16.7  0  1    5    2
# Lotus Europa  30.4   4  95.1 113 3.77 1.513 16.9  1  1    5    2

Строки внутри цикла for могут потребовать некоторого объяснения. Предположим, что call присваивается следующим образом:

call <- quote(y == x)
str(call)
# language y == x

Мы хотим подставить значение 4 для x в call. Но простой способ не работает, поскольку мы хотим содержимое call, а не символ call.

substitute(call, list(x=4))
# call

Итак, мы создаем нужное выражение, используя другой вызов substitute.

substitute(substitute(expr, list(x=4)), list(expr=call))
# substitute(y == x, list(x = 4))

Теперь у нас есть объект языка, который описывает то, что мы хотим сделать. Все это оставило это на самом деле:

eval(substitute(substitute(expr, list(x=4)), list(expr=call)))
# y == 4