关于JS的PTC问题的评论

260 阅读8分钟

PTC(Proper tail call)是唯一一个写在ES6标准中但至今(2023年12月)绝大多数 JavaScript 引擎都不支持的特性。为什么会这样,我2017年时就做过分享。这次是因为 FrankHB 在其文章《为什么我不喜欢苹果》的最后一部分提到了这个事情,所以我重新写了个评论,全文照录如下:

【鉴于本文所需之背景知识,根据读者的程度不同,可自行按序阅读:0. 关于 PTC 的概念解释; 1. 我2017年在第四届函数式编程分享会的演讲(下面第一段有录像链接);2. FrankHB 的原文;3. 本评论】


关于 PTC/STC 的争议,可以看一下我好几年前的演讲 v.youku.com/v_show/id_X… 。即使已经过了那么多年,我当时演讲里的大部分内容仍然是适用的。

作为TC39代表(尽管这个事情发生时我还不是委员会代表),我略微总结几点供参考:

第一,委员会里不同人对 PTC/STC 的态度是很不一样的。以我看来这个争议到最后更多的是JS引擎厂商之间的政治斗争。非引擎厂商的代表可能有所偏好,但基本上无论是PTC/STC是都可以接受的。比如我,我是倾向于STC的(具体原因后面说),但一定要上PTC我也不反对。具体到每个代表,其考量是多方面的,包含了一些非技术因素。即使单说技术因素,考虑的方向也会很不同,比如我评价JS的语言特性时非常看重其在普通JS程序员在工程实践上的后果(其实我这种出发点在委员会是少数派)。其他人则也各有不同的出发点,相比较来说,反而是引擎厂商的诉求是相对一致的。不过在这个具体事件上,如我所说,因为已经演变成了引擎厂商之间的政治斗争,其他因素就不重要了。

第二,所谓「其中一些实际阻碍 PTC 实现的工程理由就是错的」。首先,工程上可以实现不代表就是可接受的方案。尤其有一些因素可能被其他的争论掩盖了。比如跨越特定边界(如 js/wasm,还有 membrane —— 这玩意儿比较难用一句话解释清楚,反正就是某种隔离层)的问题,我记得mozilla的人反对PTC似乎主要是这个理由。其次(实际有可能是更重要的),在具体会议讨论上扔出来的理由,未必是真正的理由,或者至少不是主要理由。V8和SpiderMonkey的人难道不知道shadow stack吗,肯定是知道的(因为在吵架之前,webkit的人早就写过文章吹嘘他们自己的实现了)。所以这无非是辩论双方把所有有利于己的都讲出来,不利于己的都回避。实际上苹果的人也回避了一些对他们不利的问题。

第三,就事论事,我讲讲为什么我支持 STC 而不是 PTC。首先,PTC 导致程序员很难看出一个代码到底是不是 tail call(相比较而言,STC 因为有语法标志而非常显式)。这进一步导致在重构代码时容易破坏 PTC(STC 则不会,因为如果破坏了会是 syntax error)。糟糕的是破坏 PTC 的效果是不容易发现的。是不是 stack overflow 由很多因素决定,比如递归的次数,引擎的 stack 大小,不同引擎,不同的runtime(如mobile browser通常就比 desktop browser的stack要小)都不同,甚至其他因素都相同,如果在工具链上换了个工具,比如说js压缩工具,结果就可能是不一样的。从工程上看,就是容易引入难以发现和追踪的线上故障甚至事故。其次,需要注意 JS 的最主要应用场景 web 前端中用户浏览器的多样性。即使所有引擎都支持了 PTC,也需要非常长的时期(现在一般要两、三年,如果是中国的话那可能要5年以上)才会等到绝大多数用户都升级完,一些特殊场景则永远不会完成全部升级。因此在一个长时期里一份相同代码可能按照 PTC 也可能不按照 PTC 运作。这导致很多东西会发生偏差,比如收集来的 stack 日志根据是否支持 PTC 会产生差异。特别注意这是无意中发生的(绝大部份开发运维根本没有意识到从某一刻开始事情突然不同了)。这和 STC 的情况不同,STC 作为新语法它强制你必须对其作出处理,虽然比 PTC 麻烦,但是更显式的处理(比如根据 feature detection 分派到两个不同版本)避免了前述问题。而且不用新特性的人根本不会被打扰。实际上,由于相同代码事实上必然会存在有的 client 有 PTC,有的 client 没有 PTC ,使得「PTC 是保证而不是优化」这句话在实践上根本不成立,从而成为了一个陷阱。归根到底,STC显式地编码意图是更好的选择,至少对JS是这样(因为JS程序员传统上并不熟悉许多fp语言里有的设施和概念,贸然引入 PTC 可能不是惊喜而是惊吓)。

最后,有些东西可能是可以争辩的,但标准委员会的运作本来就要求妥协。演变成政治斗争,在我看来更大的责任在Apple。Apple既无法说服其他任何一家厂商,也无法说服利益无关方。从流程上说,在2015年之后的流程里,PTC根本达不到stage3。Apple说因为这是ES6的内容所以不需要按照新流程,虽然是个理由,但是更类似于法庭辩论中钻法律漏洞的情况。苹果真正拒绝STC的理由,在我看来根本不是不是某种对编程语言理念的坚持,而纯粹是不想其已经实现(并夸耀)的一些实现方案(如shadow stack)被标准否定从而要被丢弃。

对于开发者来说,能力比形式重要。实际结果是,自2017年至今,JS开发者始终没有普遍的PTC可用(除非你只为safari编程),也没有STC可用。这个锅只能是苹果的。(虽然很多时候v8的独断专行更甚一筹。)

BTW,「PTC 决定大量常见语言特性的原生支持是冗余的,极大地降低语言设计可触及的复杂度下限。」「消除语言设计者及实现者和普通开发者之间的技术壁垒。这也支持了本文的主要观点:用户和开发者不具有绝对的差异」……这些单独看我都是赞同的。但是对于JS来说是不成立的,因为JS的主要应用场景决定了其要永远保持兼容性,也就是即使其有了 PTC 或者代数效果之类的牛逼特性,也不可能把原有的哪些「冗余」特性删除,也就是原则上说,你不可能通过增加特性来降低JS语言的复杂性。除非老特性过于有害,而社区达成共识用linter之类的工程手段防止未来的代码中再使用。这门槛是很高的,在我看来 PTC 或代数效果去替代的那些成熟特性,只是达成了语言设计上的优雅性,但对于程序员本身的价值恐怕并不大,至少不可能让大多数JS程序员认同。包括消除语言设计者和开发者的壁垒,对于lisp来说是成立的,对于js(或大多数主流编程语言)恐怕是不成立的,也不可能是这些语言的主要设计目标。当然你可以说JS程序员「专业能力普遍不足」所以活该用烂语言,我是不会反对的,毕竟整个世界都建筑在草台班子之上。


以上。

需要说明的是,FrankHB 的这一段文字包含了大量细节,需要海量的背景知识,以至于他自己都写了「因为无法指望消除的理解门槛,大多数读者建议跳过」,那就更别指望读者能识别其中的谬误了。我这里并没有(像我年轻时写文章时)逐句评论各处问题,是因为我并不想过度纠缠细节,如果有兴趣的读者对原文中的一些细节有疑问可以留言,我会尽力解释。