Я хочу создать функцию myfun
, которую можно использовать только внутри другой функции, в моем случае dplyr
s mutate
или summarise
. Я больше не хочу полагаться на dplyr
с внутренностями (например mask$...
).
Я придумал быстрый и грязный обходной путь: функция search_calling_fn
, которая проверяет все имена функций в стеке вызовов и ищет определенный шаблон в вызывающих функциях.
search_calling_fn <- function(pattern) {
call_st <- lapply(sys.calls(), `[[`, 1)
res <- any(unlist(lapply(call_st, function(x) grepl(pattern, x, perl = TRUE))))
if (!res) {
stop("`myfun()` must only be used inside dplyr::mutate or dplyr::summarise")
} else {
return()
}
}
Это работает, как и ожидалось, как показано в двух приведенных ниже примерах (dplyr
= 1.0.0).
library(dplyr)
myfun <- function() {
search_calling_fn("^mutate|^summarise")
NULL
}
# throws as expected no error
mtcars %>%
mutate(myfun())
myfun2 <- function() {
search_calling_fn("^select")
NULL
}
# throws as expected an error
mtcars %>%
mutate(myfun2())
В этом подходе есть одна лазейка: myfun
можно вызывать из функции с похожим именем, которая не является функцией dplyr
. Интересно, как я могу проверить, из какого пространства имен приходит функция в моем стеке вызовов. rlang
имеет функцию call_ns
, но она будет работать только в том случае, если функция явно вызывается с package::...
. Кроме того, при использовании mutate
в стеке вызовов есть mutate_cols
внутренняя функция и mutate.data.frame
метод S3 — кажется, что оба делают получение пространства имен еще более сложным.
С другой стороны, мне интересно, есть ли лучший, более официальный подход для достижения того же результата: разрешить вызов myfun
только в пределах dplyr
s mutate
или summarise
.
Подход должен работать независимо от того, как вызывается функция:
mutate
dplyr::mutate
Дополнительное примечание
После обсуждения ответа @r2evans я понимаю, что решение должно пройти следующий тест:
library(dplyr)
myfun <- function() {
search_calling_fn("^mutate|^summarise")
NULL
}
# an example for a function masking dplyr's mutate
mutate <- function(df, x) {
NULL
}
# should throw an error but doesn't
mtcars %>%
mutate(myfun())
Таким образом, функция проверки должна не только смотреть на стек вызовов, но и пытаться увидеть, из какого пакета исходит функция в стеке вызовов. Интересно, что отладчик RStudios показывает пространство имен для каждой функции в стеке вызовов, даже для внутренних функций. Интересно, как он это делает, ведь environment(fun))
работает только с экспортированными функциями.
Придирка: вам не хватает закрытия }
в конце функции search_calling_fn
. — person TimTeaFan schedule 06.07.2020
Спасибо, что заметили это! Я исправил это. — person TimTeaFan schedule 06.07.2020
Связано: 1 и 2. — person TimTeaFan schedule 06.07.2020
Ваш пример кода mutate
никогда не даст сбоев, потому что x
ленив; поскольку он никогда не используется, он никогда не реализуется, поэтому myfun
никогда не вызывается. … но я понимаю вашу точку зрения, getAnywhere
в моем ответе слишком нетерпелив. — person TimTeaFan schedule 08.07.2020
Обновление: я позаимствую у
rlang::trace_back
, так как у него есть элегантный (и работающий) метод определения полногоpackage::function
для большей части дерева вызовов (некоторые, например,%>%
, не всегда полностью- решено).(Если вы пытаетесь уменьшить раздувание пакета… хотя маловероятно, что у вас будет
dplyr
, а неpurrr
доступен, если вы предпочитаете делать как можно больше в базе, я предоставил#==#
эквивалентные вызовы base-R. Безусловно, можно попытаться удалить некоторые из вызововrlang
, но опять же… если вы предполагаетеdplyr
, то у вас определенно естьrlang
, и в этом случае это не должно быть проблемой.)Цель состоит в том, чтобы вы могли искать определенные пакеты и/или определенные функции. Аргумент
funcs=
может быть фиксированной строкой (принимаемой дословно), но, поскольку я подумал, что вы, возможно, захотите сопоставить любую из функцийmutate*
(и т. д.), вы также можете сделать его регулярным выражением. Все функции должны быть полнымиpackage::funcname
, а не толькоfuncname
(хотя вы, конечно, можете сделать это регулярным выражением :-).И производительность, кажется, значительно лучше, чем в первом ответе, хотя все же не нулевой удар по производительности:
(Эта часть сохранена для процветания, хотя обратите внимание, что
getAnywhere
найдетdplyr::mutate
, даже если указанный выше не-dplyrmutate
определен и вызван.)Посеянный ссылками Руи, я предполагаю, что поиск определенных функций может очень хорошо пропустить новые функции и/или допустимые, но другие функции с другими именами. (У меня нет четкого примера.) Отсюда рассмотрите возможность поиска конкретных пакетов вместо конкретных функций.
Поймите, что это не дешевая операция. Я считаю, что большая часть времени, потраченного на это, связана с деревом вызовов, возможно, это не то, что мы можем легко исправить.
Если вы считаете, что она будет вызываться нечасто и ваша функция занимает немного времени, то, возможно, задержка в полсекунды будет не так заметна, но с этим игрушечным примером разница ощутима.
Спасибо! Вы правы, в общем случае мы не должны проверять имена функций и полагаться только на проверку происхождения функции. Для моего случая
dplyr
я придумал две альтернативы (см. ниже). — person TimTeaFan; 06.07.2020Хотя мне нравится этот подход, я только что понял, что на самом деле это не проверка пакета функции в стеке вызовов. Он просто ищет имя функции во всех загруженных пространствах имен. Таким образом, если
dplyr
загружен, но функцияmutate
замаскирована другой функцией, вызовmyfun()
внутри не-dplyrmutate
не приведет к ошибке. — person TimTeaFan; 07.07.2020Я не согласен с вашим утверждением, что он ищет во всех загруженных пространствах имен. В частности, если я делаю
library(dplyr)
, я могу выполнитьmutate(mtcars, myfun())
без ошибок, аtransform(mtcars, myfun())
выдает ошибку, несмотря на то, чтоdplyr
явно находится в пути поиска. Точно так жеtransform(mutate(mtcars), myfun())
терпит неудачу, так как не находитdplyr
в прямой цепочке вызовов. С чего вы взяли, что это просто поиск загруженных пространств имен? — person TimTeaFan; 08.07.2020Я хотел сказать, что
search_calling_pkg
просматривает стек вызовов и для каждого имени функции, которое он находит, он ищет, где (getAnywhere
) он может его найти, и это включает все загруженные пространства имен, потому что если выmutate <- function(df, x) {NULL}
, а затем вызоветеmutate(myfun())
в то время какdplyr
привязан к пути поискаmyfun()
не выдаст ошибку, хотя вы не звоните dplyr’уmutate
. — person TimTeaFan; 08.07.2020Интересно, что отладчик в RStudio покажет пространство имен для каждой функции в стеке вызовов даже для внутренних функций. Интересно, как он получает эту информацию.
environment(fun))
например работает только с экспортированными функциями. — person TimTeaFan; 08.07.2020Ваш пример
transform(mutate(mtcars), myfun())
выдает ошибку, потому что при вызовеmyfun()
mutate()
нет в стеке вызовов. Но в моем примереmyfun()
находитmutate
в стеке вызовов, но не видит, что это неправильныйmutate
, потому что перечислены все загруженные пакеты и пространства имен, в которых есть функция с именемmutate
. — person TimTeaFan; 08.07.2020Это лишний повод предположить, что моя функция работает, не так ли? Возможно, я не понимаю ваших намерений со всем этим (на самом деле, я не понимаю предпосылку, стоящую за требованием вызова функции внутри одной из функций
dplyr
). Было бы полезно привести четкие примеры, где должна возникать ошибка, а где не должна (поскольку я думаю, что запутался). — person TimTeaFan; 08.07.2020Я обновил свой вопрос дополнительным примечанием, надеюсь, это прояснит мою точку зрения. — person TimTeaFan; 08.07.2020
Спасибо за обновленный ответ, это отлично работает. Если невозможно использовать внутренние компоненты
rlang
(например, в пакете), то лучшим вариантом, вероятно, будет проверка пространства имен только с помощьюrlang::env_name(environment(fun = ...))
(см. мой обновленный ответ ниже). — person TimTeaFan; 13.07.2020Выше @r2evans показывает, как можно решить общий вопрос о том, как проверить, вызывается ли функция из другого
package::function()
.Если кто-то не хочет полагаться на внутренние функции
rlang
, возможным обходным путем является использованиеrlang::env_name(environment(fun = ...))
, однако в этом случае можно проверить только пространство имен/пакет вызывающей функции, а не имя функции:Создано 13 июля 2020 г. в пакете reprex (v0.3.0)
Для моей конкретной проблемы, чтобы проверить, вызывается ли функция из
dplyr
, я придумал эффективную альтернативу, используя вызовacross()
в качестве теста, вызывается лиmyfun()
изdplyr
. В отличие отmask$...
и т. д.across()
является экспортируемой функциейdplyr
.Создано 06 июля 2020 г. с помощью пакета reprex (v0.3.0)