Преобразование позиционных аргументов в именованные параметры в функции R на основе имени переменной
В R есть общая схема вызова функций, которая выглядит следующим образом:
child = function(a, b, c) {
a * b - c
}
parent = function(a, b, c) {
result = child(a=a, b=b, c=c)
}
Это повторение имен полезно, потому что оно предотвращает потенциально коварные ошибки, если бы порядок аргументов дочернего элемента должен был измениться, или если бы дополнительная переменная была добавлена в список:
childReordered = function(c, b, a) { # same args in different order
a * b - c
}
parent = function(a, b, c) {
result = childReordered(a, b, c) # probably an error
}
Но это становится громоздким, когда имена аргументов становятся длинными:
child = function(longVariableNameA, longVariableNameB, longVariableNameC) {
longVariableNameA * longVariableNameB - longVariableNameC
}
parent = function(longVariableNameA, longVariableNameB, longVariableNameC) {
child(longVariableNameA=longVariableNameA, longVariableNameB=longVariableNameB, longVariableNameC=longVariableNameB)
}
Я хотел бы иметь способ обеспечить безопасность именованных параметров без необходимости фактически вводить имена снова. И я хотел бы иметь возможность сделать это, когда я могу изменить только родитель, а не ребенок. Я хотел бы представить что-то вроде этого:
parent = function(a, b, c) {
result = child(params(a, b, c))
}
куда params()
будет новая функция, которая преобразует безымянные аргументы в именованные параметры на основе имен переменных. Например:
child(params(c,b,a)) == child(a=a, b=b, c=c)
В pryr есть пара функций, которые близки к этому, но я не понял, как объединить их, чтобы сделать то, что я хочу. named_dots(c,b,a)
возвращается list(c=c, b=b, a=a)
, а также standardise_call()
имеет аналогичную операцию, но я не выяснил, как можно преобразовать результаты во что-то, что может быть передано неизмененному child()
,
Я хотел бы иметь возможность использовать смесь неявных и явно именованных параметров:
child(params(b=3, c=a, a)) == child(b=3, c=a, a=a)
Было бы также неплохо иметь возможность смешивать некоторые неназванные константы (не переменные) и обрабатывать их как именованные аргументы при передаче дочернему элементу.
child(params(7, c, b=x)) == child(a=7, c=c, b=x) # desired
named_dots(7, c, b=x) == list("7"=7, c=c, b=x) # undesired
Но в не-R смысле я бы предпочел выдавать ошибки, а не пытаться разобраться с ошибками программиста:
child(params(c, 7, b=x)) # ERROR: unnamed parameters must be first
Существуют ли инструменты, которые уже существуют для этого? Простые способы собрать воедино существующие функции, чтобы сделать то, что я хочу? Лучшие способы достижения той же цели - обеспечение безопасности при наличии изменяющихся списков параметров без громоздкого повторения? Улучшения в моем предложенном синтаксисе, чтобы сделать его еще безопаснее?
Предварительное уточнение: оба parent()
а также child()
функции следует считать неизменяемыми. Я не заинтересован в обертывании с другим интерфейсом. Скорее я ищу здесь способ написать предложенный params()
функция в общем случае, которая может переписать список аргументов на лету так, чтобы оба parent()
а также child()
может использоваться напрямую с безопасным, но не многословным синтаксисом.
Разъяснение после вознаграждения: хотя инвертирование отношений "родитель-потомок" и использование do.call() является полезной техникой, я здесь ее не ищу. Вместо этого я ищу способ принять аргумент '...', изменить его на именованные параметры, а затем вернуть его в форме, которую примет включающая функция. Возможно, что, как другие предполагают, это действительно невозможно. Лично я в настоящее время думаю, что это возможно с расширением уровня C, и я надеюсь, что это расширение уже существует. Возможно, vadr
Пакет делает то, что я хочу? https://github.com/crowding/vadr
Частичная заслуга: я чувствую себя глупо, просто позволяя щедрости истечь. Если нет полных решений, я награжу его всем, кто дает подтверждение концепции хотя бы одного из необходимых шагов. Например, изменив аргумент '...' внутри функции, а затем передав его в другую функцию без использования do.call(). Или возвращая неизмененный аргумент "..." таким образом, чтобы родитель мог его использовать. Или что-нибудь, что наилучшим образом указывает путь к прямому решению, или даже некоторые полезные ссылки: http://r.789695.n4.nabble.com/internal-manipulation-of-td4682090.html Но я не хочу присуждать это ответ, который начинается с (в противном случае совершенно разумного) предположения, что "вы не хотите этого делать" или "это невозможно, поэтому есть альтернатива".
Награда: Есть несколько действительно полезных и практических ответов, но я решил наградить crowding. Хотя он (вероятно, правильно) утверждает, что то, что я хочу, невозможно, я думаю, что его ответ наиболее близок к "идеалистическому" подходу, к которому я стремлюсь. Я также думаю, что его пакет vadr может стать хорошей отправной точкой для решения, независимо от того, соответствует ли оно моим (потенциально нереалистичным) целям разработки или нет. "Принятый ответ" все еще может быть найден, если кто-то найдет способ сделать невозможное. Спасибо за другие ответы и предложения, и, надеюсь, они помогут кому-то собрать воедино части для более надежного синтаксиса R.
6 ответов
Я думаю, что попытка переписать встроенную функцию сопоставления аргументов R
несколько опасно, поэтому вот решение, которое использует do.call
,
Было неясно, сколько parent
изменчив
# This ensures that only named arguments to formals get passed through
parent = function(a, b, c) {
do.call("child", mget(names(formals(child))))
}
Второй вариант, основанный на "магии" write.csv
# this second option replaces the call to parent with child and passes the
# named arguments that have been matched within the call to parent
#
parent2 <- function(a,b,c){
Call <- match.call()
Call[[1]] <- quote(child)
eval(Call)
}
Вы не можете изменить параметры функции изнутри вызова функции. Следующим лучшим способом было бы написать простую обертку вокруг вызова. Возможно, что-то подобное может помочь
with_params <- function(f, ...) {
dots <- substitute(...())
dots <- setNames(dots, sapply(dots, deparse))
do.call(f, as.list(dots), envir=parent.frame())
}
И мы можем проверить с чем-то вроде
parent1 <- function(a, b, c) {
child(a, b, c)
}
parent2 <- function(a, b, c) {
with_params(child, a, b, c)
}
child <- function(a, b, c) {
a * b - c
}
parent1(5,6,7)
# [1] 23
parent2(5,6,7)
# [1] 23
child <- function(c, a, b) {
a * b - c
}
parent1(5,6,7)
# [1] 37
parent2(5,6,7)
# [1] 23
Обратите внимание, что parent2
является надежным для изменения в порядке параметра child
в то время как parent
является.
Нелегко получить точный синтаксис, который вы предлагаете. R лениво вычисляется, поэтому синтаксис, появляющийся в аргументе функции, просматривается только после запуска вызова функции. К тому времени, когда переводчик встречает params()
он уже начал звонить child()
и связал все child
аргументы и выполнены некоторые из child
код. Не могу переписать child
аргументы после того, как лошадь покинула сарай, так сказать.
(Большинство ленивых языков не позволяли бы вам сделать это по разным причинам)
Таким образом, синтаксис должен быть чем-то, что имеет ссылки на 'child' и 'params' в своих аргументах. vadr
имеет %()%
оператор, который может работать. Он применяет список точек, заданный справа, к функции, приведенной слева. Так что вы бы написали:
child %()% params(a, b, c)
где params
ловит его аргументы в списке точек и манипулирует ими:
params <- function(...) {
d <- dots(...)
ex <- expressions(d)
nm <- names(d) %||% rep("", length(d))
for (i in 1:length(d)) {
if (is.name(ex[[i]]) && nm[[i]] == "") {
nm[[i]] = as.character(ex[[i]])
}
}
names(d) <- nm
d
}
Это должен был быть комментарий, но он не соответствует ограничениям. Я рекомендую вас за ваши амбиции и чистоту программирования, но я думаю, что цель недостижима. Давайте предположим params
существует и применить его к функции list
, По определению params
, identical(list(a = a, b = b, c = c) , list(params(a, b, c))
, Из этого следует, что identical(a, params(a, b, c))
взяв первый элемент первого и второго аргумента identical
, Из чего следует, что params
не зависит от его второго и последующих аргументов, противоречие. КЭД Но я думаю, что ваша идея является прекрасным примером СУХОГО в R, и я очень доволен do.call(f, params(a,b,c))
, который имеет дополнительный do.call, но не повторение. С вашего разрешения я хотел бы включить его в свой пакет bettR, в котором собраны различные идеи по улучшению языка R. Связанная идея, с которой я играл, - это создание функции, которая позволяет другой функции получать недостающие аргументы из вызывающего фрейма. То есть вместо звонка f(a = a, b = b)
можно позвонить f()
и внутри f
было бы что-то вроде args = eval(formals(f), parent.frame())
но заключен в макроподобную конструкцию args = get.args.by.name
или что-то подобное. Это отличается от вашей идеи тем, что требует f
быть запрограммированным сознательно, чтобы иметь эту функцию.
Вот ответ, который на первый взгляд будет работать. Диагностика того, почему это не так, может привести к пониманию того, почему это невозможно (подсказка: см. Первый абзац ответа @crowding).
params<-function(...) {
dots<-list(...)
names(dots)<-eval(substitute(alist(...)))
child.env<-sys.frame(-1)
child.fun<-sys.function(sys.parent()+1)
args<-names(formals(child.fun))
for(arg in args) {
assign(arg,dots[[arg]],envir=child.env)
}
dots[[args[[1]]]]
}
child1<-function(a,b,c) a*b-c
parent1<-function(a,b,c) child1(params(a,b,c))
parent1(1,2,3)
#> -1
child2<-function(a,c,b) a*b-c #swap b and c in formals
parent2<-function(a,b,c) child2(params(a,b,c)) #mirrors parent1
parent2(1,2,3)
#> -1
Оба производят 1*2-3 == -1
хотя порядок b=2
а также c=3
были поменяны местами в списке формальных аргументов child1
против child2
,
Это в основном разъяснение ответа Мнела. Если это случится, чтобы ответить на ваш вопрос, пожалуйста, не принимайте его и не присуждайте награду; это должно идти к Mnel. Сначала мы определяем call_as_parent
, который вы используете для вызова функции внутри другой функции в качестве внешней функции:
call_as_parent <- function(fun) {
par.call <- sys.call(sys.parent())
new.call <- match.call(
eval(par.call[[1L]], parent.frame(2L)),
call=par.call
)
new.call[[1L]] <- fun
eval(new.call, parent.frame())
}
Затем мы определяем parent и child:
child <- function(c, b, a) a - b - c
parent <- function(a, b, c) call_as_parent(child)
И напоследок несколько примеров
child(5, 10, 20)
# [1] 5
parent(5, 10, 20)
# [1] -25
Обратите внимание, как четко во втором примере 20 соответствует c
, как это должно.