ISO - хороший способ позволить функции принимать сочетание предоставленных аргументов, аргументов из списка и значений по умолчанию - PullRequest
0 голосов
/ 19 ноября 2018

Я хотел бы, чтобы функция принимала аргументы обычным способом R, большинство из которых будут иметь значения по умолчанию.Но я также хотел бы, чтобы он принял список именованных аргументов, соответствующих некоторым или некоторым или всем формальным формам.Наконец, я хотел бы, чтобы аргументы, передаваемые в функцию напрямую, а не через список, переопределяли аргументы списка в тех случаях, когда они конфликтуют.

Я мог бы сделать это с помощью нескольких вложенных операторов if.Но у меня есть ощущение, что есть какое-то элегантное, лаконичное решение для программирования на языке R-ish - возможно, несколько таких решений - и я хотел бы научиться их использовать.Чтобы показать, какое решение я ищу:

> arg_lst <- list(x=0, y=1)
> fn <- function(a_list = NULL, x=2, y=3, z=5, ...){
   <missing code>
   print(c(x, y, z))
  }

> fn(a_list = arg_list, y=7)

Желаемый вывод:

x  y  z
0  7  5

Ответы [ 3 ]

0 голосов
/ 19 ноября 2018

Я не уверен, насколько это "элегантно", но вот моя лучшая попытка удовлетворить требования ОП.Логика if / else на самом деле довольно проста (не требует вложенности, как таковой).Реальная работа заключается в сборе и дезинфекции трех различных типов ввода (формальные значения по умолчанию, объект списка и любые предоставленные аргументы).

fn <- function(a_list = NULL, x = 2, y = 3, z = 5, ...) {

  formal_args <- formals() # get the function's defined inputs and defaults
  formal_args[names(formal_args) %in% c('a_list', '...')] <- NULL # remove these two from formals
  supplied_args <- as.list(match.call())[-1] # get the supplied arguments
  supplied_args['a_list'] <- NULL # ...but remove the argument list

  # for each uniquely named item among the 3 inputs (argument list, defaults, and supplied args):
  for (i in unique(c(names(a_list), names(formal_args), names(supplied_args)))) {
    if (!is.null(supplied_args[[i]])) {
      assign(i, supplied_args[[i]])
    } else if (!is.null(a_list[[i]])) {
      assign(i, a_list[[i]])
    }
  }

    print(c(x, y, z))
}

arg_lst <- list(x = 0, y = 1)
fn(a_list = arg_lst, y=7)

[1] 0 7 5

Немного больше углубляясь в функции метапрограммирования R, на самом деле этоМожно упаковать это иерархическое назначение в свою собственную функцию, которая предназначена для работы в функциональной среде, которая его вызывает.Это облегчает повторное использование этой функции, но она определенно нарушает область видимости и должна рассматриваться как опасная.

Функция «иерархического назначения», в основном такая же, как и раньше:

hierarchical_assign <- function(a_list) {

  formal_args <- formals(sys.function(-1)) # get the function's defined inputs and defaults
  formal_args[names(formal_args) %in% c('a_list', '...')] <- NULL # remove these two from formals
  supplied_args <- as.list(match.call(sys.function(-1), sys.call(-1)))[-1] # get the supplied arguments
  supplied_args['a_list'] <- NULL # ...but remove the argument list

  # for each uniquely named item among the 3 inputs (argument list, defaults, and supplied args):
  for (i in unique(c(names(a_list), names(formal_args), names(supplied_args)))) {
    if (!is.null(supplied_args[[i]])) {
      assign(i, supplied_args[[i]], envir = parent.frame())
    } else if (!is.null(a_list[[i]])) {
      assign(i, a_list[[i]], envir = parent.frame())
    }
  }
}

Ииспользование.Обратите внимание, что вызывающая функция должна иметь аргумент с именем a_list, и он должен быть передан hierarchical_assign.

fn <- function(a_list = NULL, x = 2, y = 3, z = 5, ...) {

    hierarchical_assign(a_list)

    print(c(x, y, z))
}

[1] 0 7 5
0 голосов
/ 19 ноября 2018

Мне очень нравится подход @ jdobres, но мне не нравится использование assign и возможные перерывы в области видимости.

Мне также не нравится предпосылка, что функция должна быть написана специальным образом, чтобы это работало.Не лучше ли написать обертку, наподобие do.call, для работы с любой функцией?Вот такой подход:

Редактировать: решение, основанное на purrr::invoke

Подумав немного об этом, purrr::invoke почти получим - но это приведетв ошибке, если аргумент списка также передан в ....Но мы можем внести небольшие изменения в код и получить рабочую версию более кратко.Эта версия кажется более надежной.

library(purrr)
h_invoke = function (.f, .x = NULL, ..., .env = NULL) {
    .env <- .env %||% parent.frame()
    args <- c(list(...), as.list(.x))  # switch order so ... is first
    args = args[!duplicated(names(args))] # remove duplicates
    do.call(.f, args, envir = .env)
}

h_invoke(fn, arg_list, y = 7)
# [1] 0 7 5

Оригинальная версия, в значительной степени заимствованная из кода jdobres:

hierarchical_do_call = function(f, a_list = NULL, ...){
   formal_args = formals() # get the function's defined inputs and defaults
   formal_args[names(formal_args) %in% c('f', 'a_list', '...')] = NULL # remove these two from formals
   supplied_args <- as.list(match.call())[-1] # get the supplied arguments
   supplied_args[c('f', 'a_list')] = NULL # ...but remove the argument list and the function
   a_list[names(supplied_args)] = supplied_args
   do.call(what = f, args = a_list)
}

fn = function(x=2, y=3, z=5) {
  print(c(x, y, z))
}

arg_list <- list(x=0, y=1)
hierarchical_do_call(f = fn, a_list = arg_list, y=7)
# x  y  z
# 0  7  5

0 голосов
/ 19 ноября 2018

Я думаю do.call() делает именно то, что вы хотите.Он принимает функцию и список в качестве аргументов, причем список является аргументами для функций.Я думаю, что вам понадобится функция-обертка для создания такого поведения "перезаписи значений по умолчанию"

...