Как управлять параметрами и аргументами в R?

Это довольно простой и общий вопрос, но я не видел, чтобы это уже обсуждалось. Надеюсь, я ничего не пропустил.

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

Рассмотрим этот простой пример: simple schema

Подфункция интереса SF() многократно запрашивается с помощью разных аргументов "WORKER", но с теми же параметрами, которые установлены "выше". Конечно, тот же вопрос относится к более сложным случаям с несколькими слоями.

Я вижу два способа борьбы с этим: 1. Передача всего, но:  а. Вы получите множество аргументов в вызове функции или структуру, включающую все эти аргументы.  б. Поскольку R делает копии аргументов для вызова функций, это может быть не очень эффективно. 2. Динамически оценивать функции каждый раз, когда вы меняете параметры, и "убирать" их в определение функции. Но я не уверен, как это сделать, особенно в чистом виде.

Ничто из этого не кажется действительно симпатичным, поэтому мне было интересно, есть ли у вас, ребята, мнение по этому поводу? Может быть, мы могли бы использовать некоторые экологические особенности R?:-)

Спасибо!

РЕДАКТИРОВАТЬ. Поскольку для некоторых код лучше, чем графики, вот пример фиктивного примера, в котором я использовал метод "1.", передавая все аргументы. Если у меня много слоев и подфункций, передача всех параметров промежуточным слоям (здесь WORKER()) кажется невелика. (с точки зрения кода и производительности)

F <- function(){
  param <- getParam()
  result <- WORKER(param)
  return(result)
}

getParam <- function(){
  return('O')
}

WORKER <- function(param) {
  X <- LETTERS[1:20]
  interm.result <- sapply(X,SF,param) # The use of sapply here negates maybe the performance issue?
  return(which(interm.result=='SO'))
}

SF <- function(x,param) {
  paste0(x,param)
}

EDIT 2. Простота приведенного выше примера вводит в заблуждение некоторых из тех, кто смотрит на мою проблему, так что вот более конкретная иллюстрация, использующая дискретный градиентный спуск. Опять же, я сохранил это просто, поэтому все можно было записать в одной и той же большой функции, но это не то, что я хочу сделать для моей реальной проблемы.

gradientDescent <- function(initialPoint= 0.5, type = 'sin', iter_max = 100){ 
  point <- initialPoint
  iter <- 1
  E <- 3
  deltaError <- 1
  eprev <- 0
  while (abs(deltaError) > 10^(-2) | iter < iter_max) {
    v_points <- point + -100:100 / 1000
    E <- sapply(v_points, computeError, type)
    point <- v_points[which.min(E)]
    ef <- min(E)
    deltaError <- ef - eprev
    eprev <- ef
    iter <- iter+1
  }
  print(point)
  return(point)
}

computeError <- function(point, type) {
  if (type == 'sin') {
    e <- sin(point)
  } else if (type == 'cos') {
    e <- cos(point)    
  }
}

Я считаю неоптимальным передавать параметр "type" подфункции каждый раз, когда он оценивается. Похоже, что ссылка, приводимая @hadley на Closures и объяснение @Greg, - хорошие пути к решению, которое мне нужно.

Ответ 1

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

Если вы не можете воспользоваться лексическим охватом (SF должен быть определен вне F), другой вариант - создать новую среду для хранения ваших параметров, а затем, если все необходимые функции имеют доступ к этой среде (либо передавая явно или по наследству (делая эту среду охватывающей средой функций), то F может назначить param в эту среду, а другие функции могут получить доступ к значению.

Ответ 2

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

Конечно, учитывая задачу, показанную в вашем примере, я бы сделал нечто подобное:

SF <- function(x, par) {
    paste0(x, par)
}

F <- function(param) {
    which(sapply(LETTERS[1:20], FUN = SF, par = param) == "SO")
}

F(param="O")
#  S 
# 19 

Или, используя лексическое определение, которое Грег Сноу назвал:

F <- function(param) {
    SF <- function(x) {
         paste0(x, param)
    }
    which(sapply(LETTERS[1:20], FUN = SF) == "SO")
}
F(param="O")

Или, в действительности, и пользуясь тем, что paste0() векторизован:

F <- function(param) {
    which(paste0(LETTERS[1:20], param) == "SO")
}
F("O")
# [1] 19

Я понимаю, что мой ответ может показаться слишком упрощенным: у вас явно есть что-то более сложное, но я думаю, вам нужно лучше показать нам, что это такое. Чтобы получить дополнительную помощь, я предлагаю вам следовать рекомендациям во втором комментарии @baptiste, давая нам менее абстрактный пример и объясняя, почему вы вызываете F() и getParam() без каких-либо аргументов (а также, возможно, демонстрируете, почему вам нужна функция getParam() вообще).

Ответ 3

Даже несмотря на то, что этот вопрос седел, я подумал, что было бы полезно затронуть пару других способов, с которыми я видел, эта проблема решена и дать ответ в слоте ответа. Обратите внимание, что просмотр и сообщение шаблона не совпадает с его одобрением!

Затворы

Как упоминалось в комментариях и исправленном ответе, closures, безусловно, является хорошим ответом здесь. То есть вы можете определить функцию в функции, то есть функцию генератора и передать информацию из функции генератора в сгенерированную функцию.

generator_function <- function(param) {
  function() {
    param   
  }
}

generated_function <- generator_function(0)
generated_function()

В контексте вопроса это может рекомендовать определение computeError внутри gradientDecent, тогда computeError может переносить type в свою среду.

Как только вы закроете закрытие, я думаю, вы обнаружите, что они довольно мощные. Тем не менее, им немного сложно подумать. Более того, если вы не привыкли к ним, и сгенерированная функция заканчивается развязанной от входов функции генератора, они могут быть немного сложными для отладки, поскольку может возникнуть путаница в отношении того, что такое значение type и где оно появилось из. Чтобы помочь первой проблеме, я сердечно рекомендую pryr::unenclose. Во-вторых, я позволю более мудрым умом, чем мой колокол, если возникнет такая необходимость.

Задайте опцию

Часто необработанные параметры устанавливаются как опция (c.f. ?options) либо напрямую, либо через функции getter/setter (например, knitr). Тем не менее, я также видел функции, установленные как параметры, а также снова и снова. Лично мне не нравится этот шаблон, потому что он выполняется довольно непротиворечиво по пакетам, и параметры обычно зарываются в документацию определенных функций, но ваш фактический вызов может быть для функции более высокого уровня, когда опция, необходимая для выполнения этой функции более высокого уровня то, что вы хотите, может быть захоронено в документах для функции более низкого порядка.

...

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

Другие

Конечно, есть и другие шаблоны! Тонны из них. Люди проходят вокруг окружения и строят списки и т.д. И т.д. Какой ответ "правильный" для вас, вероятно, представляет собой смесь вашего личного стиля, что работает, и что будет ясно для вас, когда вы вернетесь и посмотрите на него через несколько месяцев.