是莽撞人就来单挑:关于自动格式化 R 代码的求助

谢益辉 2017-02-26

2021-03-22 更新

下面的第一个愿望已经在 formatR 1.9 中实现。

2021-05-27 更新

下面的第二个愿望已经在 formatR 1.11 中实现。

2010 年我发布了一个叫 formatR 的 R 包,它可以把乱糟糟的 R 代码自动清理成大致整齐的代码(例如自动加空格、换行等),这个包也是我当年做 knitr 的小小动力之一。虽然 formatR 是个小麻雀包,但它的开发过程中也有三个里程碑事件:

  1. 最早的时候,怡轩想出了一个非常聪明的黑魔法,让这个整理过程能尽量保留代码注释;

  2. 后来 Kohske Takahashi 大人提供了一个很漂亮的黑魔法,可以让赋值的等号自动被替换为左箭头;

  3. 等啊等,终于等到 R 里面出现了 getParseData() 函数,让我终于不用红着脸用正则表达式去解析代码中的注释了,而是可以大大方方用官方 API 去解析;

然而,我对这个包还有两个小小的夙愿一直没能实现。虽不至于让我晚上睡不着觉,但若能实现,那真真是极好的。第一个愿望是,我希望能(尽量)控制代码的最大宽度。目前 formatR::tidy_source() 的宽度参数 width.cutoff 源自 deparse() 函数,而这个参数的大意是尝试在多少个字符宽度之后尝试断行,而我希望能在多少个字符宽度之前断行。因为如果之后断行,万一那个位置上恰好有个很长的变量名不能被折断,那就要等到这个变量名之后才能尝试换行,于是代码可能会超出右边界很多,这对需要 PDF 印刷品输出的写作来说是个灾难,调整起来很费神。

第二个愿望是如果小括号里的参数太多的话,就在小括号之后直接断行(原因在前面的日志中解释过),比如:

my_sum = function(
  a = 1, b = 2, c = 3, d = 4, e = 5,
  f = 6, g = 7
) {
  return(a + b + c)
}

my_sum(
  a = 1, b = 2, c = 3, d = 4, e = 5,
  f = 6, g = 7
)

目前 tidy_source() 函数只能做到:

my_sum = function(a = 1, b = 2, c = 3, d = 4, e = 5,
  f = 6, g = 7) {
  return(a + b + c)
}

my_sum(a = 1, b = 2, c = 3, d = 4, e = 5,
  f = 6, g = 7)

当然这个不算糟糕,我可以忍。这两个愿望我更迫切需要实现第一个。客官们可能会想能不能给 R 里面的 deparse() 函数打个补丁,让它既支持最大宽度也支持最小宽度,如果能打补丁并且 R 核心团队愿意接受的话,那也是极好的,但显然我没有这个技能,因为 deparse() 的源代码是基于 C 的,我一看一千多行就两眼蒙圈,但我有一定的把握去游说一位核心团队成员接受它。如果想靠自己造轮子的话,那么应该就是两条可能的路1:一是 getParseData()(注意它有坑会截断长字符串),二是用 sourcetools 包,这个我没用过,但看起来也许比 getParseData() 更容易操纵一点,而且作者是敝司的同事,有问题可以直接找他解决。

若客官们还有闲情逸致,可以顺便把这两个功能也一并实现了(#57#62)。好了,黑客骚年们,我在世上最大的同姓交友网站上等你们的合并请求。


  1. 其实还有第三条非常黑的路,就是暴力尝试不同宽度,看能否找到一个合适的,这个我当年我迫于 R CMD check 的压力早已经在 Rd2roxygen 包中试过了。所以不要跟我比黑招,我琢磨起黑招来我自己都感到害怕。除非是被逼急了,别一言不合就暴力求解。 ↩︎