一行代码千般喜

谢益辉 2019-04-11

几年前我说过一个《两个字符千行泪》的故事。我在谈论软件开发时,似乎谈论的忧桑故事多一些,可能是因为别扭的事不吐不快,而喜悦的事情则可以暗自独享。今日挑选一行今年我自认为比较得意的代码,参见 GitHub 上的这则改动。这一行代码扫除了一个恶心了我自己两年多的问题。我把它敲出来的时候,觉得自己过去两年可能是脑子被驴踢过,为何没早想到这个呢。

还得交代一下背景故事,不然客官们几乎不大可能看懂这行代码的意义。在 servr 包中,我有一个监听本地文件变化的服务器函数 servr::httw(),这在各类服务器程序里很常见,功能就是当服务的文件(HTML/CSS/JS/图片之类的)有更新时,就通知浏览器自动刷新页面。这就省得你每次在编辑器里编辑文件后,还得挪动鼠标去浏览器里点刷新按钮查看你的改动。你只需要把浏览器丢在旁边,只在编辑器里编辑、保存文件即可。术语上这叫实时刷新(Live-reload)。

这个实现并不困难,我在《用 R 创建网页服务器》一文中已经略有提及,就是通过 WebSocket 让 R 与浏览器通信。浏览器每隔一段时间(比如一秒)就问问 R:你那边的文件有没有变动?R 用 list.files()file.info() 算一下,若回答有,那么浏览器端就执行 JS 代码 location.reload() 刷新页面。这就是大致原理。

因为 R 需要比较当前文件信息和上一次的文件信息,所以我们需要把上一次的文件信息保存在一个变量里。前面提的那行代码中的双箭头就是干这事的:

info <<- info2

其中 info 是外层变量,用来记录上一次的文件信息,info2 是内层变量,计算当前文件信息。那以前是什么问题恶心着我自己呢?这问题是,我要实现动态编译 R Markdown 文档;当 Rmd 文件更新时,我要先把它重新编译为 HTML,然后再在浏览器里刷新这个 HTML 文件。问题就出在编译上:因为编译可能会出错,比如 R 代码还没写完整就保存导致报错;报错之后我不能下一秒再继续重新编译,因为用户可能在一秒内还修复不了错误,所以过去我用的一个机制是模仿了 Gmail 掉线之后重试的时间间隔机制,也就是先等两秒再重试,两秒后还有问题再等四秒,然后等八秒、十六秒……直到错误被修正、文档能正确编译出来。

刚开始实现这个机制时,我甚至都快为自己的机智手动点赞了,我特么真呀么是个天才呀我,居然联想到了借鉴 Gmail 的办法。然而等我真的在自己 R Markdown 文档中不慎搞出个错误来时,我立刻就觉得自己快把自己蠢哭。看到红色的错误消息每隔一秒、两秒、四秒、八秒……一次次冒出来的时候,我彻底心方了,只能手忙脚乱赶紧思考我是谁、我从哪里来、晚饭要吃什么、到底哪里写错了、咋修正,比考试还紧张。

两个月前我在捣鼓究极无限月读时,突然想到:我特么为什么要不管不顾地重试编译一个有错误的文档呢?如果编译出错,那就先等着用户修改好文档保存之后再重试呀,骚年!他要是没重新保存,那我就该按兵不动,让他有充足的时间去找出错误并修正。

之前之所以会一遍遍重试编译,是因为我在重编译之前没有刷新一下上次的文件信息变量,即上面的 info,而是等编译成功之后才刷新,这就导致每当出错时,上一次的文件信息还是旧的(出错前的),而下次当浏览器问 R 的时候,R 仍然觉得文件有变动,所以不断重新编译一个错文档。这个问题的修正就是那么简单:在每次尝试重编译之前就刷新一下文件信息变量,成功后也照常刷新一遍,所以加一行代码就搞定了。

除了不再让用户感到莫名紧张,这个改动还让我成功扔掉了一大坨当年我觉得聪明的代码(也就是模仿 Gmail 的代码)。对码农而言,删代码永远都比添代码舒心得多。也是这个原因,今年我在 rticles 包中删掉了无数行代码(例一例二),觉得终于把这个包归置到可维护的程度了。之前我们厂长写的代码冗余度高得令人发指,我忍了好几次终于忍不住了,大刀阔斧砍瓜切菜抽丝剥茧,把它调理成了逻辑基本统一的包,往后别人贡献新模板也更简单了。

尽管我不是故意制造问题再解决问题,但这千般喜实质上跟弗洛伊德说的那种“在寒冷的夜晚把脚伸出被窝冻一会儿再缩回来的幸福”一样。说到底,只不过是我自己先蠢到一个极致(就是蠢到觉得还有点小聪明的程度),最后又发现这个蠢可以轻易解除,从而人为造就了满足感。