Вложенный do.call в среде foreach %dopar% не может найти функцию, переданную с помощью.export

Я вкладываю несколько уровней do.call (каждая из них использует функции, названные в параметрах, а не жестко закодированные) в %dopar% параллельная среда, и функция из моей внешней среды не может быть найдена самой внутренней функцией. Я знаю о .export параметр на foreach и я использую его, но каким-то образом названная функция не распространяется по всей цепочке.

Я сократил мою проблему до следующего контрольного примера, который действительно показывает эту проблему:

library(doParallel)
cl <- makeCluster(4)
registerDoParallel(cl)

simple.func <- function(a, b) {
  return(a+b)
}

inner.func <- function(a, b) {
  return(do.call(simple.func, list(a=a, b=b)))
}

outer.func <- function(a, b, my.func=inner.func) {
  return(do.call(my.func, list(a=a, b=b)))
}

main.func <- function(my.list=1:10, my.func=outer.func,
                      my.args=list(my.func=inner.func)) {
  results <- foreach(i=my.list, .multicombine=TRUE, .inorder=FALSE,
                     .export="simple.func") %dopar% {
    return(do.call(my.func, c(list(a=i, b=i+1), my.args)))
  }
  return(results)
}

Вместо того, чтобы дать правильный ответ (список с некоторыми цифрами), я получаю:

Error in { : task 1 failed - "object 'simple.func' not found" 

Добавление if (!exists("simple.func")) stop("Could not find parse.data in scope main.func") в начале каждой функции (при необходимости изменяя название области видимости) показывает, что это inner.func который не видит simple.func -- даже если outer.func действительно видит это.

Я также проверил несколько вариантов выше, с main.func или же outer.func жестко закодировать функцию следующего уровня, а не использовать ее из параметра. Оба эти варианта работают (например, дают ожидаемый результат), но для реального случая я хочу сохранить обобщенность принятия подфункций в качестве параметров.

# Variation number one: Replace main.func() with this version
main.func <- function(my.list=1:10, my.func=outer.func,
                      my.args=list(my.func=inner.func)) {
  results <- foreach(i=my.list, .multicombine=TRUE, .inorder=FALSE,
                     .export=c("simple.func", "outer.func", "inner.func")) %dopar% {
    return(do.call(outer.func, list(a=i, b=i+1, my.func=inner.func)))
  }
  return(results)
}

# Variation number two: Replace outer.func() and main.func() with these versions
outer.func <- function(a, b, my.func=inner.func) {
  return(do.call(inner.func, list(a=a, b=b)))
}

main.func <- function(my.list=1:10, my.func=outer.func,
                      my.args=list(my.func=inner.func)) {
  results <- foreach(i=my.list, .multicombine=TRUE, .inorder=FALSE,
                     .export=c("simple.func", "inner.func")) %dopar% {
    return(do.call(my.func, c(list(a=i, b=i+1), my.args)))
  }
  return(results)
}

Я мог бы также пройти simple.func вниз по цепочке вручную, включив его в качестве дополнительного параметра, но это выглядит очень грязно, и почему это должно быть необходимо, когда simple.func нужно просто передать как часть окружающей среды?

# Variation number three: Replace inner.func(), outer.func(), and main.func()
# with these versions
inner.func <- function(a, b, innermost.func=simple.func) {
  return(do.call(innermost.func, list(a=a, b=b)))
}

outer.func <- function(a, b, my.func=inner.func,
                       innermost.args=list(innermost.func=simple.func)) {
  return(do.call(my.func, c(list(a=a, b=b), innermost.args)))
}

main.func <- function(my.list=1:10, my.func=outer.func,
                      my.args=list(my.func=inner.func,
                      innermost.args=list(innermost.func=simple.func))) {
  results <- foreach(i=my.list, .multicombine=TRUE, .inorder=FALSE,
                     .export="simple.func") %dopar% {
    return(do.call(my.func, c(list(a=i, b=i+1), my.args)))
  }
  return(results)
}

У кого-нибудь есть идеи для менее смелых решений или первопричина этой проблемы?

1 ответ

Решение

За doParallelи любой другой doNnn адаптер, который не разворачивает текущий процесс, я думаю, что следующий хак сделает это:

main.func <- function(my.list = 1:10, my.func=outer.func,
                      my.args = list(my.func=inner.func)) {
  results <- foreach(i = my.list, .multicombine = TRUE, .inorder = FALSE,
                     .export="simple.func") %dopar% {
    environment(my.args$my.func) <- environment()  ## <= HACK
    return(do.call(my.func, args = c(list(a=i, b=i+1), my.args)))
  }
  return(results)
}

В качестве альтернативы вы можете использовать адаптер doFuture (я автор). Тогда вам не нужно беспокоиться о глобальных объектах, потому что они автоматически идентифицируются и экспортируются. То есть нет необходимости указывать .export (или же .packages). Например, в вашем случае работает следующее:

library("doFuture")
registerDoFuture()
plan(multisession, workers = 4)

main.func <- function(my.list = 1:10, my.func = outer.func,
                      my.args = list(my.func = inner.func)) {
  foreach(i = my.list, .multicombine = TRUE, .inorder = FALSE) %dopar% {
    do.call(my.func, args = c(list(a = i, b = i+1), my.args))
  }
}

res <- main.func(1:3)
str(res)
## List of 10
##  $ : num 3
##  $ : num 5
##  $ : num 7

Вы также можете пропустить foreach() все вместе и делаем:

library("future")
plan(multisession, workers = 4)

main <- function(my.list = 1:10, my.func = outer.func,
                 my.args = list(my.func = inner.func)) {
  future_lapply(my.list, FUN = function(i) {
    do.call(my.func, args = c(list(a = i, b = i+1), my.args))
  })
}

PS. Есть много разных plan() бэкэнды на выбор. Единственное, что не покрывается, если вы используете doRedis,

Другие вопросы по тегам