Феерическая расстановка точек над нестандартным вычислением в R
1. Функции quote()
, substitute()
и eval()
Рассмотрим три основные функции, которые обеспечивают механизм нестандартного вычисления. Следующие простые примеры показывают, как эти функции ведут себя при вызове в глобальном окружении (при вызове внутри других функций будут свои особенности - см. ниже).
1.1. Использование quote()
, substitute()
и eval()
в глобальном окружении
В глобальном окружении функции quote()
и substitute()
ведут себя одинаково:# При передаче в качестве аргумента какого-либо значения
# возвращается само это значение:
quote(5)
# [1] 5
str(quote(5))
# num 5
# При передаче в качестве аргумента переменной возвращается имя этой
# переменной в виде объекта, имеющего тип "symbol" и класс "name":
quote(a)
# a
str(quote(a))
# symbol a
typeof(quote(a))
# [1] "symbol"
class(quote(a))
# [1] "name"
# При передаче в качестве аргумента выражения возвращается это
# выражение в виде объекта, имеющего тип "language" и класс "call":
quote(mean(1:100)+a)
# mean(1:100) + a
str(quote(mean(1:100)+a))
# language mean(1:100) + a
typeof(quote(mean(1:100)+a))
# [1] "language"
class(quote(mean(1:100)+a))
# [1] "call"
# При передаче в качестве аргумента какого-либо значения
# возвращается само это значение:
substitute(5)
# [1] 5
str(substitute(5))
# num 5
# При передаче в качестве аргумента переменной возвращается имя этой
# переменной в виде объекта, имеющего тип "symbol" и класс "name":
substitute(a)
# a
str(substitute(a))
# symbol a
typeof(substitute(a))
# [1] "symbol"
class(substitute(a))
# [1] "name"
# При передаче в качестве аргумента выражения возвращается это
# выражение в виде объекта, имеющего тип "language" и класс "call":
substitute(mean(1:100)+a)
# mean(1:100) + a
str(substitute(mean(1:100)+a))
# language mean(1:100) + a
typeof(substitute(mean(1:100)+a))
# [1] "language"
class(substitute(mean(1:100)+a))
# [1] "call"
Функция eval()
по своему действию противоположна двух предыдущим и всегда возвращает результат вычисления переданного ей выражения:a <- 1
eval(1)
# [1] 1
eval(a)
# [1] 1
eval(quote(a))
# [1] 1
eval(mean(1:100)+a)
# [1] 51.5
eval(quote(mean(1:100)+a))
# [1] 51.5
Подробнее про типы и классы см. в Classes Corresponding to Basic Data Types.
1.2. Использование quote()
, substitute()
и eval()
внутри другой функции
Функция quote()
внутри другой функции работает так же, как и в глобальном окружении, т.е. всегда возвращает объекты типа “symbol” или “language”.Функция
eval()
- тоже, но тут уместно рассмотреть ее дополнительные аргументы envir
и enclos
. envir
задает окружение, в котором выполняется выражение, причем в роли окружения может выступать список или таблица data.frame как частный случай списка. enclos
задает окружение, в котором происходит поиск объектов, не найденных в envir
, но только если в envir
в качестве окружения задан список или таблица. Если в envir
задано “настоящее” окружение, то enclos
игнорируется.sample_df <- data.frame(a = 1:5, b = 5:1, c = c(5, 3, 1, 4, 1))
d <- 11:20 # переменная, которой нет в sample_df
fun <- function(df, var) {
var_call <- substitute(var)
eval(var_call, df, parent.frame())
# parent.frame() - окружение, из которого вызывается fun()
}
fun(df = sample_df, var = a)
# [1] 1 2 3 4 5
# a из sample_df
fun(df = sample_df, var = d)
# [1] 11 12 13 14 15 16 17 18 19 20
# d из глобального окружения, в котором происходит вызов fun()
При вызове substitute()
внутри другой функции в возвращаемом выражении происходит замена переменных на их значения, если это возможно. Вначале ищутся переменные в теле функции; для переменных, которые не определены в теле функции, но чьи имена соответствуют именам формальных аргументов, берутся соответствующие значения аргументов; переменные, определенные вне функции, а также нигде не определенные, остаются в виде своих имен:b <- 2 # значение b недоступно из fun()
fun <- function(x, y) {
a <- 1 # значение a берется из тела функции
y <- 100 # значение y берется из тела функции, а не из аргумента
substitute(x+y+a+b+d) # значение x берется из аргумента
}
fun(3, 4)
# 3 + 100 + 1 + b + d
2. Представление аргументов функций: объекты типа “promise”
Функцияsubstitute()
, а также нестандартное вычисление и “ленивые” вычисления в целом работают благодаря специальному объекту типа “promise”, который создается при вызове функции, имеет имя формального аргумента и содержит в себе выражение, переданное в качестве значения этого аргумента. Формальное описание можно найти в R Language Definition - 2.1.8 Promise objects. Рассмотрим простейший пример в виде функции с двумя формальными аргументами:fun <- function(x, y) {
# При вызове функции fun(x = a, y = b + 1) создается:
# объект "promise" с именем "x", содержащий
# невычисленное выражение (имя переменной) "a";
# объект "promise" с именем "y", содержащий
# невычисленное выражение "b + 1".
# Не происходит никакого сопоставления имен и проверки,
# существует ли вообще где-то "a" или "b + 1".
z <- 23*24^2 # вычисления без обращений к
z <- sqrt(z) # значениям аргументов
substitute(x) # вернет "symbol" "a" (без кавычек), т.е. имя переменной
quote(x) # вернет "symbol" "x" (без кавычек), т.е. имя аргумента
# (что, очевидно, будет бесполезным для функции)
x <- x^2 # только здесь происходит подстановка значения "a"
# (разумеется, если переменная "a" будет найдена)
y <- 100 # y определен в теле функции
# (выражение "b + 1" останется невычисленным)
return(c(x, y))
}
a <- 2
rm(b) # удаляем переменную b
fun (x = a, y = b)
# [1] 1 100
# Отсутствие переменной b на работе функции не сказывается.
Пример чуть посложнее (сокращенный вариант из “Advanced R”):sample_df <- data.frame(a = 1:5, b = 5:1, c = c(5, 3, 1, 4, 1))
y <- 4
x <- 4
condition <- 4
condition_call <- 4
subset2 <- function(x, condition) {
condition_call <- substitute(condition)
r <- eval(condition_call, x)
x[r, ]
}
subset2(sample_df, a == 4)
#> a b c
#> 4 4 2 4
subset2(sample_df, a == y)
#> a b c
#> 4 4 2 4
subset2(sample_df, a == x)
#> a b c
#> 1 1 5 5
#> 2 2 4 3
#> 3 3 3 1
#> 4 4 2 4
#> 5 5 1 1
#> NA NA NA NA
#> NA.1 NA NA NA
subset2(sample_df, a == condition)
#> Error in eval(expr, envir, enclos): object 'a' not found
Неожиданный результат вызова subset2(sample_df, a == x)
объясняется просто: функция находит x
внутри себя, потому что x
является именем формального аргумента:subset2(x = sample_df, a == x)
# Внутри происходит следующее:
condition_call <- substitute(a == x)
condition_call
# a == x
r <- eval(condition_call, x)
r
# a b c
# [1,] TRUE FALSE FALSE
# [2,] TRUE FALSE FALSE
# [3,] TRUE TRUE FALSE
# [4,] TRUE FALSE TRUE
# [5,] TRUE FALSE FALSE
# Значение переменной "a" было найдено внутри "x" (x = sample_df,
# т.е. таблица sample_df выступает в роли окружения).
# Значение "x" не найдено внутри "x", но найдено в вышестоящем окружении:
# при вызове функции был создан объект типа "promise" с именем "x", содержащий
# имя переменной sample_df, а sample_df у нас есть в глобальном окружении.
# Таким образом, было выполнено сравнение x$a == x.
Теперь по аналогии легче разобраться с subset2(sample_df, a == condition)
, особенно если переписать вызов как subset2(x = sample_df, condition = a == condition)
.Здесь функция
eval()
пытается выполнить выражение a == condition
(оно присвоено переменной condition_call
) в окружении x
, а x
у нас является таблицой sample_df
. Т.е. ожидается, что a
и condition
будут именами столбцов.Столбец с именем “a” будет найден, а столбец с именем “condition” - нет, поэтому поиск соответствия этому имени переменной будет продолжен в вышестоящем окружении, а именно - в окружении выполнения функции
subset2()
.Уточнение: окружение выполнения является “виртуальным” окружением, которое создается в момент вызова функции и обычно исчезает после окончания ее работы (но сохраняется при использовании замыканий, когда внутри функции создаются новые функции).
Итак, в этом “виртуальном” окружении есть объекты типа “promise”, подобные описанным выше, с именами “x” (соответствует присвоению
x <- sample_df
) и “condition” (соответствует присвоению condition <- a == condition
).Тогда при обращении к
condition
происходит попытка вычисления выражения a == condition
, но выражение a == condition
в данном окружении (“снаружи” таблицы sample_df) вычислить нельзя, поскольку там нет переменной а
. Нет такой переменной и в еще более вышестоящих окружениях, в данном случае - в глобальном. Поэтому получаем ошибку.Если создать в глобальном окружении переменную
a
(a <- 4
), то функция будет работать, но возращать совсем не то, что нужно.Вызов функции с нестандартным вычислением внутри другой функции
Вновь возьмем упрощенный пример из “Advanced R” и попробуем понять, почему первый вариант со вложенными функциями не работает, а второй - работает.Первый вариант:
subset2 <- function(x, condition) {
condition_call <- substitute(condition)
r <- eval(condition_call, x, parent.frame())
x[r, ]
}
subscramble <- function(x, condition) {
subset2(x, condition)
}
subscramble(sample_df, a >= 4)
# Error in eval(expr, envir, enclos) : object 'a' not found
Второй вариант:subset2_q <- function(x, condition) {
r <- eval(condition, x, parent.frame())
x[r, ]
}
subscramble <- function(x, condition) {
condition <- substitute(condition)
subset2_q(x, condition)
}
subscramble(sample_df, a >= 4)
# a b c
# 4 4 2 4
# 5 5 1 1
Подробнейшее объяснение было найдено на StackOverflow в ответе Josh O’Brien-а: Non standard evaluation from another function in R. На мой взгляд, его сообщение является образцом исследования технических и скрытых от глаз пользователей (да и программистов, в большинстве случаев) особенностей функционирования языка. Ну а я попробую пересказать простыми словами.В первом варианте мы вызываем функцию в виде
subscramble(x = sample_df, condition = a >= 4)
. До того, как происходит вызов внутренней функции subset2()
, создаются объекты типа “promise”: x
(содержит невычисленное выражение sample_df
) и condition
(содержит невычисленное выражение a >= 4
).Именно эти объекты передаются внутренней функции в качестве значений: когда вызывается внутренняя функция
subset2()
, ее вызов выглядит как subset2(x = x, condition = condition)
, а не как subset2(x = sample_df, condition = a >= 4)
.Это эквивалентно передаче функции позиционных аргументов, т.е. функция
subset2()
ищет в окружении, откуда она вызывается (и в вышестоящих), объекты с именами x
и condition
- и находит там оба наши объекта типа “promise”. Тогда substitute(condition)
вернет просто condition
, а не a >= 4
. Переменной condition
внутри sample_df
, разумеется, нет, и начинается поиск соответствия в вышестоящем окружении.Такое соответствие будет найдено внутри функции
subscramble()
(в “виртуальном” окружении, созданном в момент вызова функции). Дальше наконец-то происходит попытка вычисления выражений, хранящихся в объектах типа “promise”, и возникает закономерная ошибка, ведь a >= 4
вычислить снаружи функции subset2()
невозможно, там нет переменной a
.Во втором варианте внутренняя функция
subset2()
имеет доступ к объекту сondition
типа “language”, т.е. непосредственно к выражению, которое нужно вычислить; substitute(condition)
здесь выполняется как substitute(a >= 4)
и возвращает a >= 4
. Фактически происходит нечто похожее:# Используется создание новой переменной в теле внешней функции.
fun <- function(x, y) x+y
fun1 <- function(x, y) {
y <- y^2
fun(x, y)
}
fun1(1,2)
# [1] 5
Спасибо за статью, очень полезный разбор. Может быть, можно бы ещё про get() добавить хоть немножко?
ОтветитьУдалитьПожалуйста :)
УдалитьНе знаю, чего бы такого интересного я мог бы написать про эту функцию. Мне кажется, встроенной справки достаточно.
ну, я имел в виду не "интересного", а вообще про место и использование этой функции в контексте "нестандартных вычислений" - мне казалось, что для полноты картины она так же важна, как и остальные разобранные здесь функции.
УдалитьЕсли не сложно текст программ печатать в стиле bold/ На сером фоне светло серый текст сложновато читать.
ОтветитьУдалитьПока что не придумал более удачной цветовой схемы, чем стандартная; для чтения можете взять исходники в репозитории https://github.com/statist-bhfz/r_exp/tree/master/nse
Удалить