别在生产环境中用净土

谭显英

2019/11/14

每次系统性地升级R包,我最担心的就是tidyverse相关的代码,因为不时地就会出现各种"break the existing code"的升级,令人非常头疼。果然不出所料,原来合法的代码又一次报错:

这个代码会报错(但是之前是不会的)

dplyr::mutate_at(data.frame(a = 1), "a", .funs = list(~as.character))

必须改成如下

dplyr::mutate_at(data.frame(a = 1), "a", .funs = list(as.character))

或者

dplyr::mutate_at(data.frame(a = 1), "a", .funs = list(~as.character(.)))

我个人大概是从2014年1月开始用dplyr。它在某种程度上更接近SQL一些,对于新手比较友好,而且效率上确实比base里面的函数要高不少。再加上后来一度被所谓的 “可以直接在dplyr语法中实现对数据库的操作——从而避免了自己手写SQL语句” 的幻想所“洗脑”,在长达1~2年的时间内大批量地使用tidyverse(其实主要就是dplyr)。

渐渐地就发现tidyverse最大最大的问题就在于,它还是不够“成熟”,每过一段时间就会出现“毁灭性”地升级。然而,它又是一个处理数据的工具,基本上每一个脚本里都会用很多次,导致每一次的更改都极其痛苦和麻烦。

另外,dplyr虽然可以用相同的语法来操作数据库,我不否认其自身也是蛮有用的。但是,“能够真的避免自己手写SQL语句”也的确只是幻想。原因是,每一种语言都有丰富的细节,而这些细节是实际问题处理时必须要精确控制的。然而,不同语言的细节是不可能完全一致的,导致许多在R里能表达的在SQL里很难直接表达,反之亦然。用dplyr来操作数据库这种做法只能用来研究,一旦真正用于生产环境,不可避免地就会遇到许多"corner cases"——比如dplyr::filter(FIELD %in% character(0L))根本无法生成可靠的SQL语句来达到返回0行数据框的结果。而这些"corner cases"处理起来不胜其烦,最后发现如果刚开始就用SQL写,根本就不会有这么多的麻烦。

还有,我开始放弃这种无止境地使用管道%>%的习惯了。刚开始,确实觉得这种方式真的挺清晰的。然而,实际的数据集哪里有示例那么简单的情况,各种转换处理非常复杂。引入适量的具有很好命名的中间变量,才能让程序的可读性提高。况且,一旦程序出现bug,使用管道的程序debug真是不方便。当然,管道也有其优势,比如在shiny相关的代码中,我还是会经常使用。

最后,tidyverse现在包的依赖太多,这又导致一个问题就是出现错误的时候,很难返回清晰的错误提示,有时候traceback()回去也不知道究竟是哪里的问题。

除了以上几点问题外,dplyr这种语法的设计虽然相对而言比较容易上手,但是也导致了它不适合处理金融行业常见的时间序列数据,而且在效率上没法达到最优。而data.table则不同,它本身的语法比dplyr更简洁,这导致了它能够在一个函数里表达更加丰富的意图,从而使得对于时间序列的有些操作更方便,也更高效。当然,最关键的原因是,data.table异常稳定——这和它本身几乎零依赖也有很大关系。我个人在2016年年初就已经下定决心不再使用tidyverse,而是全方位转为使用data.table,并逐渐把既有的一些程序重写。但是精力有限而脚本实在太多,以至未能完全完成这一计划,便有了今日的痛苦。

(我当然知道可以不用升级,但是tidyverse包之间的关系又很复杂,比如你升级了rlang,你可能就必须要用新的dplyr,不然就不行,再说不升级只是拖延问题而不是解决问题的办法,难道不是吗?)

虽然我只列举了dplyr的例子,但是tidyr、rlang、 dbplyr都有过不少不兼容旧代码的情况。其实,我个人对于升级是非常欢迎的,偶尔需要调整下源代码,我也并不觉得有问题。然而,如果是dplyr::mutate()这种“一个脚本里就会被用好多次的极为常用的”函数也经常性地发生需要调整生产代码的情况,我真的是不敢再使用了,还请爱折腾的同志自己折腾去吧。

综上,暂不论tidyverse设计或语法是好是坏,但它对于我来讲真的不够成熟,强烈不推荐用在生产环境中。