如何在含可选参数的函数中使用tidydots(...)?是否为不良实践?
原函数的核心问题
你的两个函数存在几个tidyeval使用误区,同时有包开发需要规避的不良实践:
tidydots参数传递的语法混淆
第一个函数createTable的参数顺序是counts, mapper = NULL, day = NULL, week = NULL, month = NULL, ...,当你用命名传参指定mapper = "mcglonee"等参数后,后续的mapper, month, week作为位置参数会被传入...,但你实际想引用的是数据框中的列名,而这些符号在调用环境中没有对应的对象,导致group_by(!!!group_vars)无法识别列名。全局依赖(严重不良实践)
函数直接依赖全局变量summaryVars和全局函数calcScores,自用场景下可能没问题,但包开发中这会导致函数可移植性差、容易出现命名冲突,完全不符合模块化设计原则。过滤逻辑冗余与变量名冲突
你把函数参数重新赋值为m, d, w, mp的操作完全冗余,还容易引发数据框列名(比如month)和函数参数名的混淆。此外,多次调用filter()会降低代码效率,完全可以合并为一次。分组参数的错误处理方式
createTable2尝试用group_vars参数接收分组变量,但传入list(mapper, month, week)时,这些符号是在调用环境中求值,而非函数内部或数据框环境,导致R找不到对应的对象——tidyeval的核心就是捕获表达式而非求值后的对象。
优化后的函数实现
下面是修复后的函数,同时兼顾可选参数、tidydots支持,以及包开发的最佳实践:
library(dplyr) createTable <- function(counts, mapper = NULL, day = NULL, week = NULL, month = NULL, summary_vars = c("default_col1", "default_col2"), ...) { # 捕获分组变量的表达式(保留列名的符号形式) group_vars <- enquos(...) # 构建过滤条件,自动忽略空参数 filter_conds <- list( if (!is.null(mapper)) expr(mapper == !!mapper), if (!is.null(day)) expr(day == !!day), if (!is.null(week)) expr(week == !!week), if (!is.null(month)) expr(month == !!month) ) # 移除空条件(base R写法,无需额外包) filter_conds <- Filter(Negate(is.null), filter_conds) # 参数验证:确保输入合规(包开发必备) stopifnot("counts必须是tibble格式" = is_tibble(counts)) stopifnot("summary_vars必须是counts中的列" = all(summary_vars %in% colnames(counts))) counts %>% group_by(!!!group_vars) %>% # 用dplyr 1.0+推荐的across替代已软弃用的summarise_at summarise(across({{ summary_vars }}, sum), .groups = "drop_last") %>% # 一次性应用所有过滤条件 filter(!!!filter_conds) %>% # 如果calcScores是包内函数,需确保它在命名空间中;若需用户自定义,可设为参数 calcScores() }
关键改进点:
- tidyeval正确用法:用
enquos(...)捕获分组变量的符号表达式,!!明确引用函数参数(避免和数据框列名冲突),expr()动态构建过滤条件。 - 消除全局依赖:将
summaryVars改为函数参数,允许用户自定义汇总列,同时保留默认值。 - 简化逻辑提升效率:合并多次
filter()调用,用Filter自动移除空条件。 - 参数验证:添加
stopifnot确保输入合规,这是包开发中提升鲁棒性的关键步骤。
正确调用方式
无论是位置传参还是命名传参,只需将分组变量作为最后一批参数传入即可:
# 命名传参示例 test <- createTable( counts = featureCounts2019, mapper = "mcglonee", month = 5, # 分组变量(直接传入数据框中的列名符号) mapper, month, week )
包开发层面的额外注意事项
如果要将这个函数纳入R包,还需注意:
- 函数导出与命名空间:确保
calcScores作为包的导出函数或内部函数(用:::定义),避免全局依赖。 - 文档化:用
roxygen2为函数添加详细文档,说明每个参数的用途、返回值,以及示例调用代码。 - 测试覆盖:用
testthat编写测试用例,覆盖各种场景(比如不传可选参数、传多个分组变量、传入非法参数等)。 - 语法兼容性:如果需要兼容旧版dplyr,可保留
summarise_at作为备选,但优先推荐across。
内容的提问来源于stack exchange,提问作者Karl Johnson

