Использование purrr::map с пользовательской функцией – как передавать аргументы

Я создаю пакет, в котором у меня есть функция (pkgfun), которая вычисляет некоторые индикаторы на основе значений из подмножеств фрейма данных, используя group_by и purrr::map. Я также хотел бы предоставить пользователю возможность отправить дополнительную функцию (userfun) для расчета этого индикатора, если ни одна из встроенных функций не соответствует потребностям. Поэтому pkgfun должен принимать некоторые дополнительные параметры для userfun, но также позволять userfun использовать другие переменные в pkgfun, либо другие аргументы, либо переменные, определенные внутри функции.

В идеале мне бы хотелось написать функции как можно ближе к следующему примеру. Однако это не позволит правильно передать аргументы.

pkgfun = function(df, pkgArg, userfun, fargs, test, ...) {
    dots = list(...)
    funArg = 3
    fargs = names(formals(userfun))
    ufRes = df %>% 
      group_by(himgid) %>%  nest() %>%
      mutate(ufres = map(data, ~userfun(., fargs))) %>%
      unnest(ufres) %>% ungroup %>% select(ufres) %>% pull 
    print("ufRes")
    ufRes
  }
  
  userfun = function(df, aboveLimit, pkgArg, funArg) {
    print(funArg)
    sum(df$gridvar > aboveLimit) >= pkgArg
  }

 pkgfun(df, userfun = userfun, aboveLimit = 5, pkgArg = 5)

Мне удалось создать упрощенный рабочий пример, но я считаю, что это зависит от некоторой избыточности на стороне пользователя, кажется слишком сложным и использует некоторые выражения, которые я бы предпочел удалить (например, eval(parse(...))).

Реализация pkgfun допускает два способа передачи правильных аргументов, но ни один из них мне не нравится.

Первый принимает дополнительный аргумент fargs, который включает в себя все имена аргументов, которые следует добавить в argList — список аргументов. Это неприятность при вызове функции.

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

Я думаю, что можно как приблизиться к первому примеру, так и убрать конструкцию eval(parse, но как...? Какие-либо предложения?

# pkgArg is an argument that is otherwise used in pkgfun, and reused in userfun
pkgfun = function(df, pkgArg, userfun, fargs, test, ...) {
  dots = list(...)
# funArg is a variable defined inside the function, and but also useful in userfun. 
  funArg = 3
# Finding the arguments from userfun if not given as an argument
  if (missing(fargs)) {
    fargs = names(formals(userfun))
    fargs = fargs[!(fargs %in% c("df", "argList", "..."))]
  }
  dnames = names(dots)[which(names(dots) %in% fargs)] 
# Adding ... arguments to the environment, so they are available to dynGet
  if (length(dnames) > 0) for (dn in dnames) eval(parse(text = paste(dn, " = ", dots[[dn]] )))
  argList = lapply(fargs, dynGet)
  names(argList) = fargs
# 
  ufRes = df %>%
    group_by(himgid) %>%  nest() %>%
    mutate(ufRes = map(data, ~userfun(., argList = argList))) %>%
    unnest(ufRes) %>% ungroup %>% select(ufRes) %>% pull 
  print("ufRes")
  ufRes
}

# The userfun checks if the group has at least pkgArg entries that are larger than aboveLimit
# It also prints the names of the arguments in argList, and funArg, just to check that it 
# is correctly passed
userfun = function(df, aboveLimit, pkgArg, funArg, argList) {
  print(paste("argList", names(argList)))
   aboveLimit = argList$aboveLimit
   pkgArg = argList$pkgArg
   funArg = argList$funArg
   print(funArg)
    sum(df$gridvar > aboveLimit) >= pkgArg
  }

  df = data.frame(himgid = rep(c(1,2,3), 20), gridvar = 1:20)

# Call function with a vector of argument names, the named variables are then unnecessary in
# function definition
  pkgfun(df, userfun = userfun, fargs = c("aboveLimit", "pkgArg", "pkgArg"), 
            aboveLimit = 5, pkgArg = 5)
# This call is possible when the arguments are named in the function definition
  pkgfun(df, userfun = userfun, aboveLimit = 5, pkgArg = 5)

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

Jon 20.06.2024 17:27

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

Jon 21.06.2024 12:07
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
2
70
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я возьму на себя удар...

require(tidyverse)

df = data.frame(himgid = rep(c(1,2,3), 20), gridvar = 1:20)


pkgfun = function(df, pkgArg, userfun, fargs, test, ...) {

  dots = list(...)
  funArg = 3
  fargs_names = names(formals(userfun))
  
  passed_in  <- setdiff(intersect(ls(),fargs_names),"df")
  names(passed_in) <- passed_in
  args_to_pass <- map(passed_in,\(x)get(x,pos = -1)) |> setNames(passed_in)
  args_to_pass <- c(args_to_pass,dots)

  ufRes = df %>% 
    group_by(himgid) %>%  nest() %>%
    mutate(ufres = map(data, \(subdata){
      local_args <- c(list("df"=subdata),args_to_pass)
      do.call(userfun,args=local_args)})) %>%
    unnest(ufres) %>% ungroup %>% select(ufres) %>% pull 
  print("ufRes")
  ufRes
}

userfun = function(df, aboveLimit, pkgArg, funArg) {
  print(funArg)
  sum(df$gridvar > aboveLimit) >= pkgArg
}

pkgfun(df, userfun = userfun, aboveLimit = 5, pkgArg = 5)

Спасибо! Непростое решение (мне еще придется немного поработать, чтобы точно понять, как и почему), но оно, кажется, решает два наиболее важных требования, которые у меня были по сравнению с моим решением - упростить создание userfun для пользователя и удалить eval(parse

Jon 20.06.2024 18:23

одна вещь, которую я сделал, это переименовал ограничение на «overLimit» в вызове pkgfun, который вы делаете для тестирования; поскольку я не видел, как еще лимит следует интерпретировать как находящийся выше лимита.

Nir Graham 20.06.2024 18:25

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

Jon 21.06.2024 09:51

И поскольку я подробно рассмотрел различные части, я также лучше это понимаю. Очень хорошо! Однако у меня есть два вопроса о строках, которые, возможно, можно было бы упростить. Необходимо ли 'names(passed_in) <- pass_in'? И можно ли заменить строку «args_to_pass <- map(passed_in,(x)get(x,pos = -1)) |> setNames(passed_in)» на «args_to_pass <- map(passed_in, dynGet)»? Кажется, мне помогает игнорирование имен в обоих случаях, но, может быть, я что-то упускаю?

Jon 21.06.2024 09:51

1) Я допустил ошибку, решив дважды незначительную деталь, поэтому одно из решений лишнее; вы можете удалить имена (passed_in) <- pass_in или следующие имена наборов на карте; поскольку они оба существуют для достижения одного и того же аффекта, наличие одного означает, что другой не нужен.

Nir Graham 21.06.2024 11:16

2) dynGet, код выглядит немного чище, так что потенциально это хороший крик; Я думаю, единственное, что может принести пользу pos=-1, это то, что в зависимости от этого у вас могут возникнуть проблемы с поиском того, что ищете на других глубинах (или если это может быть преимуществом). Я предлагаю вам написать тесты и убедиться, что вы счастливы; и в будущем, если вы когда-нибудь столкнетесь с соответствующими ошибками, у вас будет возможность изучить их.

Nir Graham 21.06.2024 11:16

Супер, спасибо за пояснения!

Jon 21.06.2024 12:09

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