Я хочу создать функцию myfun, которую можно использовать только внутри другой функции, в моем случае dplyrs 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 только в пределах dplyrs mutate или summarise.
Подход должен работать независимо от того, как вызывается функция:
mutatedplyr::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)