R purrr :: partial - как он обрабатывает частичные аргументы? - PullRequest
3 голосов
/ 13 июля 2020

Я довольно долго был энтузиастом c пакета R purrr и недавно столкнулся с вопросом относительно purrr::partial. Предположим, я определяю функцию с двумя аргументами

f <- function(x, y) x + y

и разделяю ее, устанавливая аргумент y на значение некоторой глобальной переменной:

yy <- 1
fp <- partial(f, y = !!yy)
fp(3)                       # 3 + 1 = 4

Удаление кавычек yy (то есть использование y = !!yy вместо y = yy) приводит к тому, что yy оценивается только один раз при создании fp; в частности, изменение yy после этого шага не меняет fp:

yy <- 2
fp(3)                       # still: 3 + 1 = 4

Вот мой вопрос: что именно делает partial после оценки yy? - Я вижу две возможности:

  1. Значение yy «жестко зашито» в тело fp, что означает, что оно не передается в качестве аргумента, когда fp - вызывается.
  2. Значение yy более или менее обрабатывается так, как если бы оно было значением по умолчанию аргумента y (без возможности переопределения значения по умолчанию), что означает, что fp внутренне вызывает f (или его копию), которому значение yy незаметно передается в качестве аргумента, соответствующего y. В этом случае fp - это не более чем синтаксическая c оболочка вокруг f.

Пытаясь исследовать вторую возможность, я изменил определение f после определения fp. Это не меняет fp, что означает, что fp не содержит никаких внешних ссылок на f; однако это не исключает (теоретической) возможности того, что fp содержит копию старой версии f. (Заключение: этот подход не помогает.)

Некоторые практические предпосылки для мотивации моего вопроса: в моем текущем проекте я определил множество функций, которые используют (а) аргументы, меняющиеся от вызова к вызову, (б) аргументы представляющие «данные конфигурации» или «знания предметной области». Данные, сопоставленные с аргументами (b) (которые могут быть значительными объемами данных), не изменяются от вызова к вызову, но могут измениться, когда я фиксирую обновление; в любом случае я считаю, что эти данные не должны жестко закодироваться в моих функциях. Моя стратегия состоит в том, чтобы прочитать данные конфигурации из некоторых файлов во время запуска и интегрировать их в мои функции, частично используя аргументы в (b). Применение частичных функций через purrr::pmap к некоторым тибблам оказалось довольно медленным, что заставило меня подозревать, что данные конфигурации все еще могут передаваться при вызове функции - отсюда и мой вопрос. (Если у кого-то есть какие-то мысли по поводу кратко описанной выше «стратегии частичной реализации», они меня тоже очень заинтересуют.)

1 Ответ

1 голос
/ 13 июля 2020

Похоже, что это вариант 2. Попробуйте:

f <- function(x, y) x + y
yy <- 5
fp1 <- partial(f, y = !! yy)
debugonce(f)
fp1(3)

Здесь вы можете увидеть, что в RStudio отладчик откроет исходную функцию f, для которой аргументы x = 3 и y = 5 пройдены. Однако частичная функция вызывает не реальную функцию f, а ее копию в кавычках. Если вы измените f после того, как он был частично выделен, отладчик его больше не найдет.

f <- function(x, y) x + y
yy <- 5
fp1 <- partial(f, y = !! yy)
f <- function(x, y) x + 2 * y
debugonce(f)
fp1(3) # debugger will not open

Можно имитировать c поведение partial, построив функцию для частичной самоопределения. Однако в этом случае ни f, ни yy не захватываются, поэтому их изменение повлияет на вывод вашей частичной функции:

f <- function(x, y) x + y
yy <- 5

# similar to `partial` but captures neither `f` nor `yy`
fp2 <- function(x) f(x, yy) 
fp2(3)
#> [1] 8
# so if yy changes, so will the output of fp2
yy <- 10
fp2(3)
#> [1] 13
# and if f changes, so will the output of fp2
f <- function(x, y) x + 2 * y
fp2(3)
#> [1] 23

Создано 13.07.2020 пакет REPEX (v0.3.0)

Чтобы лучше понять, как работает partial, мы можем построить функцию simple_partial следующим образом:

library(rlang)

f <- function(x, y) x + y
yy <- 5

simple_partial <- function(.f, ...) {
  
  # capture arguments
  args <- enquos(...)
  # capture function
  fn_expr <- enexpr(.f)
  # construct call with function and supplied arguments 
  # in the ... go all arguments which will be supplied later
  call <- call_modify(call2(.f), !!! args, ... = )
  # turn call into a quosure (= expr and environment where it should be evaluated)
  call <- new_quosure(call, caller_env())
  # create child environment of current environment and turn it into a data mask
  mask <- new_data_mask(env())
  # return this function
  function(...) {
    # bind the ... from current environment to the data mask
    env_bind(mask, ... = env_get(current_env(), "..."))
    # evaluate the quoted call in the data mask where all additional values can be found
    eval_tidy(call, mask)
  }

}

fp3 <- simple_partial(f, y = !! yy)
fp3(1)
#> [1] 6

Создано 13.07.2020 пакетом REPEX. (v0.3.0)

...