Я создаю пакет, в котором у меня есть функция (pkgfun), которая вычисляет некоторые индикаторы на основе значений из подмножеств фрейма данных, используя group_by и purrr::map. Я также хотел бы предоставить пользователю возможность отправить дополнительную функцию (userfun) для расчета этого индикатора, если ни одна из встроенных функций не соответствует потребностям. Поэтому pkgfun должен принимать некоторые дополнительные параметры для userfun, но также позволять userfun использовать другие переменные в pkgfun, либо другие аргументы, либо переменные, определенные внутри функции.
В идеале мне бы хотелось написать функции как можно ближе к следующему примеру. Однако это не позволит правильно передать аргументы.
pkgfun = function(df, pkgArg, userfun, fargs, test, ...) {
dots = list(...)
funArg = 3
fargs = names(formals(userfun))
ufRes = df %>%
group_by(himgid) %>% nest() %>%
mutate(ufres = map(data, ~userfun(., fargs))) %>%
unnest(ufres) %>% ungroup %>% select(ufres) %>% pull
print("ufRes")
ufRes
}
userfun = function(df, aboveLimit, pkgArg, funArg) {
print(funArg)
sum(df$gridvar > aboveLimit) >= pkgArg
}
pkgfun(df, userfun = userfun, aboveLimit = 5, pkgArg = 5)
Мне удалось создать упрощенный рабочий пример, но я считаю, что это зависит от некоторой избыточности на стороне пользователя, кажется слишком сложным и использует некоторые выражения, которые я бы предпочел удалить (например, eval(parse(...))).
Реализация pkgfun допускает два способа передачи правильных аргументов, но ни один из них мне не нравится.
Первый принимает дополнительный аргумент fargs, который включает в себя все имена аргументов, которые следует добавить в argList — список аргументов. Это неприятность при вызове функции.
Второй проверяет список обязательных аргументов userfun, ищет их и добавляет все в argList. Однако это означает, что пользователю необходимо указать в userfun фиктивные аргументы, которые на самом деле не используются в функции.
Я думаю, что можно как приблизиться к первому примеру, так и убрать конструкцию eval(parse, но как...? Какие-либо предложения?
# pkgArg is an argument that is otherwise used in pkgfun, and reused in userfun
pkgfun = function(df, pkgArg, userfun, fargs, test, ...) {
dots = list(...)
# funArg is a variable defined inside the function, and but also useful in userfun.
funArg = 3
# Finding the arguments from userfun if not given as an argument
if (missing(fargs)) {
fargs = names(formals(userfun))
fargs = fargs[!(fargs %in% c("df", "argList", "..."))]
}
dnames = names(dots)[which(names(dots) %in% fargs)]
# Adding ... arguments to the environment, so they are available to dynGet
if (length(dnames) > 0) for (dn in dnames) eval(parse(text = paste(dn, " = ", dots[[dn]] )))
argList = lapply(fargs, dynGet)
names(argList) = fargs
#
ufRes = df %>%
group_by(himgid) %>% nest() %>%
mutate(ufRes = map(data, ~userfun(., argList = argList))) %>%
unnest(ufRes) %>% ungroup %>% select(ufRes) %>% pull
print("ufRes")
ufRes
}
# The userfun checks if the group has at least pkgArg entries that are larger than aboveLimit
# It also prints the names of the arguments in argList, and funArg, just to check that it
# is correctly passed
userfun = function(df, aboveLimit, pkgArg, funArg, argList) {
print(paste("argList", names(argList)))
aboveLimit = argList$aboveLimit
pkgArg = argList$pkgArg
funArg = argList$funArg
print(funArg)
sum(df$gridvar > aboveLimit) >= pkgArg
}
df = data.frame(himgid = rep(c(1,2,3), 20), gridvar = 1:20)
# Call function with a vector of argument names, the named variables are then unnecessary in
# function definition
pkgfun(df, userfun = userfun, fargs = c("aboveLimit", "pkgArg", "pkgArg"),
aboveLimit = 5, pkgArg = 5)
# This call is possible when the arguments are named in the function definition
pkgfun(df, userfun = userfun, aboveLimit = 5, pkgArg = 5)
Я не уверен, кто это закрыл. В любом случае, я не буду дальше модифицировать вопрос, так как на него уже получен хороший ответ, который решает наиболее важные вопросы выше.





Я возьму на себя удар...
require(tidyverse)
df = data.frame(himgid = rep(c(1,2,3), 20), gridvar = 1:20)
pkgfun = function(df, pkgArg, userfun, fargs, test, ...) {
dots = list(...)
funArg = 3
fargs_names = names(formals(userfun))
passed_in <- setdiff(intersect(ls(),fargs_names),"df")
names(passed_in) <- passed_in
args_to_pass <- map(passed_in,\(x)get(x,pos = -1)) |> setNames(passed_in)
args_to_pass <- c(args_to_pass,dots)
ufRes = df %>%
group_by(himgid) %>% nest() %>%
mutate(ufres = map(data, \(subdata){
local_args <- c(list("df"=subdata),args_to_pass)
do.call(userfun,args=local_args)})) %>%
unnest(ufres) %>% ungroup %>% select(ufres) %>% pull
print("ufRes")
ufRes
}
userfun = function(df, aboveLimit, pkgArg, funArg) {
print(funArg)
sum(df$gridvar > aboveLimit) >= pkgArg
}
pkgfun(df, userfun = userfun, aboveLimit = 5, pkgArg = 5)
Спасибо! Непростое решение (мне еще придется немного поработать, чтобы точно понять, как и почему), но оно, кажется, решает два наиболее важных требования, которые у меня были по сравнению с моим решением - упростить создание userfun для пользователя и удалить eval(parse
одна вещь, которую я сделал, это переименовал ограничение на «overLimit» в вызове pkgfun, который вы делаете для тестирования; поскольку я не видел, как еще лимит следует интерпретировать как находящийся выше лимита.
Да, вы правы, это была опечатка, когда я упрощал код примера. Я изменил все экземпляры лимита в примерах обратно на вышеуказанный лимит, чтобы он соответствовал вашему ответу.
И поскольку я подробно рассмотрел различные части, я также лучше это понимаю. Очень хорошо! Однако у меня есть два вопроса о строках, которые, возможно, можно было бы упростить. Необходимо ли 'names(passed_in) <- pass_in'? И можно ли заменить строку «args_to_pass <- map(passed_in,(x)get(x,pos = -1)) |> setNames(passed_in)» на «args_to_pass <- map(passed_in, dynGet)»? Кажется, мне помогает игнорирование имен в обоих случаях, но, может быть, я что-то упускаю?
1) Я допустил ошибку, решив дважды незначительную деталь, поэтому одно из решений лишнее; вы можете удалить имена (passed_in) <- pass_in или следующие имена наборов на карте; поскольку они оба существуют для достижения одного и того же аффекта, наличие одного означает, что другой не нужен.
2) dynGet, код выглядит немного чище, так что потенциально это хороший крик; Я думаю, единственное, что может принести пользу pos=-1, это то, что в зависимости от этого у вас могут возникнуть проблемы с поиском того, что ищете на других глубинах (или если это может быть преимуществом). Я предлагаю вам написать тесты и убедиться, что вы счастливы; и в будущем, если вы когда-нибудь столкнетесь с соответствующими ошибками, у вас будет возможность изучить их.
Супер, спасибо за пояснения!
В начале я добавил пример идеальных (но нерабочих) функций. Они ближе к тому, как, по моему мнению, должны быть реализованы функции, но чего-то не хватает. Старый пример сохранен, чтобы показать два неоптимальных способа получения нужной функциональности.