当动态语言有了类型会是怎样的编程体验
类型一直存在于我脑子里,只是有些语言需要明说,另一些语言则假装和我很有默契。
接上回,在用 JavaScript 快糙猛的蹚出一个原型后,我决定用 Scala.js 来重写,为得是让后续的迭代效率更高。但事情远没有那么简单,眼前出现是个精英怪,挡在主线情节上,绕不开,必须干。
尽管 Scala.js 让我拥有编写 Scala 的愉悦体验,但它本质是个将 Scala 代码编译成 JavaScript 代码的编译器后端扩展。也就是说, Scala 编写的部分,需要依赖的标准库和三方库,它们大多都是 JavaScript 编写的。同样,运行在 JVM 上 Scala 代码,依赖的是 Java 编写的标准库和三方库,必须考虑不同语言之间的互操作性(Interoperability)。
Scala.js 的作者是 Sébastien Doeraene 2,我发现国内某大号将其作者“颁发”给了 Li Haoyi 3,尽管他让 Scala.js 更为人所知,但他确实不是,我在此借机辅以澄清。
相比较从宽容的语言调用严格的语言,如 Javascript 调 Java 4,反过来从 Scala 调 JavaScript 就要麻烦一些。好在,这样精英怪不难打,就是费手。
往细了说,就是要为所依赖的 JavaScript 库 API 做类型适配性定义 5。类似的事情,TypeScript 也有做,这里借用官方文档的例子来说明 6:
// JavaScript
const user = {
name: "Hayes",
id: 0,
; }
// TypeScript
interface User {
: string;
name: number;
id
}
const user: User = {
: "Hayes",
name: 0,
id; }
如上适配工作,可能是细碎且一眼望不到头的,我也不想把精力放在这上面。开源社区里已经有一些前人的积累了 7,遗憾的是这些适配有可能版本落后,又或是不够完整,用起来会膈应。
上回关于语言的选择,有朋友在票圈认真评论道,“ AI coding 时代选 rust ”。按下语言选择不表,这类适配的辅助性工作,会不会才是 AI Copilot 该干的呢?
解决方案是有的,这要感谢 TypeScript 。不得不感慨,一个语言有多牛逼,还得看背后金主爸爸多有钱 8。我遇到的这个麻烦,早早地就困扰着 TypeScript 程序员 Boris ,为此 DefinitelyTyped 诞生了 9。 而后,相似故事又发生在 Scala 程序员 Oyvindberg 身上,才有了 ScalablyTyped 10。它能根据 DefinitelyTyped 中 TypeScript 类型定义,自动生成对应 Scala 类型定义。理论上,它就是个编程手柄,一键连招啊!
好是好,可也是有 隐形的认知成本
的,深究其中还蛮有趣的,我挑一个说说。TypeScript 中有个工具类型
Partial<Type>
11,典型的应用场景是,用来定义
配置选项 的对象类型,我试着用下面 Config
的代码来解释一下:
// TypeScript
interface Config {
: boolean;
editable: boolean;
showTips...
}
function input(conf: Partial<Config>): Input
Partial<Config>
意思是,任何具有部分(也可以没有)
Config
属性的对象都满足这个类型定义。通俗点讲,在调用input({...})
时,Config
所有的属性都是可选的。像这样的情况在 JavaScript
里很常见,但是不是真的都可选,没有这样的类型定义,编译器也是没法帮我们预防隐患,只能靠运行时捉虫了。
当只看了某个 JavaScript 三方库的 API 文档时,我是不知道对应的 TypeScript 定义会是怎样,更是无法预料 ScalablyTyped 又是如何转译成 Scala 的。导致在 VSCode 里查找对应的 API 方法,我那个懵圈捉急啊。真是十指不沾阳春水,看着做好的饭菜我都不知道怎么下嘴。
ScalablyTyped 的转译结果,我稍作精简如下,
// Scala
trait Config:
: Boolean
editable: Boolean
showTips...
trait PartialConfig:
: UndefOr[Boolean] = undefined
editable: UndefOr[Boolean] = undefined
showTips???
def input(conf: PartialConfig): Input
type UndefOr[A] = A | Unit
val undefined: Unit = ()
刚看上面,我是不能接受的,要我来手写会是,
// Scala
trait Config:
: UndefOr[Boolean] = undefined
editable: UndefOr[Boolean] = undefined
showTips???
def input(conf: Config): Input
对 Partial<Config>
的直译,显得画蛇添足,甚至违反直觉。转念细想,从 ScalablyTyped
角度看,其实是合理的。因为,它目的就是将 TypeScript 的翻译语义一致的
Scala, 而不关心 TypeScript 是不是从 JavaScript
那儿来的。那么,多此一举的会是 TypeScript
定义的锅吗?不好说,我更倾向把锅甩给 JavaScript ,谁叫它跟我玩默契。
以上窥见,是认知成本大,还是启用一门新语言成本大,得分情况。作为自己的项目我依然选择 Scala,但要是考虑到这个项目未来给其他人接手,嗯,不好说。
待续回见。
PS:说回本文的标题,要回答它吧,可能 TypeScript 程序员更有发言权,我这就是扔块砖吧。
https://www.graalvm.org/latest/reference-manual/js/JavaInteroperability/#access-java-from-javascript↩︎
https://www.scala-js.org/doc/interoperability/facade-types.html↩︎
https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html#defining-types↩︎
这个观点可能会引发嘴仗。这么说并不意味着可以无视语言生态里那些开源者的贡献,毕竟我自己勉强也算是其中一员。↩︎
https://johnnyreilly.com/definitely-typed-the-movie#boris-yankov↩︎
https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype↩︎