高射炮打蚊子

谢益辉 2018-05-28

在《管道时代》中我提到过杀鸡用牛刀的例子,也就是用极乐净土(Tidyverse)的工具解决本来非常初级的问题。今日看到了一个更加夸张的例子,真是吓得我不要不要的。

楼下的答主看起来是比较传统的 R 用户,对极乐净土的新势力颇有敌意,或者至少很是不耐烦,这一点从帖子的修改记录里可以看出来(一连用了三个 F 词)。嗨,这位大哥何必动怒。在网络问答社区里混,就得丢掉感情因素,尽管这比较难。你看那 COS 论坛上的 TC 君,俨然已经立地成佛,连我都开始佩服他的耐心。

我也是传统 R 用户,看到 quo(!!sym(paste0("input$", x))) 这种写法我也快晕死过去,换作 input[[x]] 简洁明了,还能直接解决问题。就我的经验来看,初级用户在使用一门动态语言时特别容易掉进一个大坑,就是用字符串的形式动态拼接成源代码,再解析执行这个字符串源代码。换句话说,也就是调用 eval(parse()) 这个黑魔法。我曾在 COS 论坛上说过,对初学者来说,eval() 只要一用基本就是错的,或者至少是非常糟糕的解决办法,并且几乎一定存在更加直接、安全的解决办法,而且通常是通过更有效的数据结构实现。

在上面的具体问题里,用户为什么会掉进这个坑呢?主要原因还是因为很多人并不知道 R 里面的列表有两种索引方式,美元符号 $ 跟名字,或者双方括号 [[]]。前一种方式在 R 世界几乎占主导地位,而在 Shiny 世界里就更流行了。它的优点在于元素的名字通常不必打引号,比如 x$a 几乎等价于 x[['a']],你看方括号的方式要比美元符号的方式多出整整五个字符,谁没事愿意多敲那么多字符。美元符号的另一大优势是在合适的编辑器中可以自动补全名字,所以不必敲全名。

当子元素的名字是一个动态值时,美元符号就不管用了,因为它后面必须跟一个固定的值。于是用户就会想到 eval(parse(text = paste0('input$', x))) 这种可怕的“有代码执行代码、没有代码创造代码也要执行代码”的拙劣方式。拜极乐净土团队的名声和影响所赐,有些追星族就以为双感叹号 !! 是万金油。唉。当然,这种问题在任何语言或方言中都会存在,所有语言都存在被滥用的可能,总会有一小批人会前赴后继入坑。

美元符号还有另外两个弊端,一是当名字不是合法变量名时,它需要被反引号引起来,比如包含空格的名字 x$`a b c` ,当然这也不能说是弊端,只是有时会迷惑到初学者;二是万恶的部分匹配(partial matching),如果问我 R 语言有哪一点是我最深恶痛绝以及不能容忍的,那便是这个了。部分匹配的优点在于让人偷懒:如果列表 x 中有一个元素 abc 而没有元素 a 的话,x$a 会自动匹配 x$abc。这个偷懒的代价太大了,会导致一些极难察觉的错误,用美元符号索引并不能保证你索引到的结果是你想要的(你想要 a 但是得到的是 abc)。相比之下,双中括号的语法永远保证精确匹配。

然鹅,美元符号在敲代码时可以节省整整五个字符啊!人一旦知道有一种可以偷懒的办法,就一定会朝那条路上走,不管路上有什么坑。好几年前我在《R 的若干基因及争论》一文中已经说过 library() 里面的两个引号问题,非常类似:一旦我们知道包名可以不用打引号,那么就不会有人再去打引号,即使理论上 library() 应该要接受的是一个字符串包名。JSS 刊物的主编们后来想逼迫作者重新把引号捡起来,这样做除了会增加主编与作者之间的小矛盾之外毫无意义。

我的主旨并不是要攻击流行的写法,而是说流行的写法在大多数情况下都没问题,但仅仅是大多数情况下而已;我们需要了解它们的缺陷,以免苦苦思索出一些复杂程度高出几个数量级的又笨又不安全的办法。