动手写一个简单的编译器:在 JavaScript 中使用 Swift 的尾闭包语法

2021-04-07 21:10:54 +08:00
 dreamapplehappy

最近业余时间在学习SwiftUI的过程中发现在SwiftUI中大量使用了尾闭包(Trailing Closure)的语法,觉得挺有趣的。作为一个经常使用JavaScript作为开发语言的前端来说,我忽然想可不可以自己写一个简单的编译器,在JavaScript中使用这种语法呢? 于是就有了这个小项目 js-trailing-closure-toy-compiler ,通过这个编译器我们可以将下面的代码:

a(){}

转换为:

a(() => {});

或者将:

a(1, "hello"){ b, c in
    d()
    d{}
    d(1, "hello")
    d(1, "hello"){}
    d(1, "hello"){ e, f in
        g()
    }
}

转换为:

a(1, "hello", (b, c) => {
    d();
    d(() => {});
    d(1, "hello");
    d(1, "hello", () => {});
    d(1, "hello", (e, f) => {
        g()
    })
})

关于Swift的尾闭包如果你不是很理解,可以参考Swift关于 Closures 的文档

项目的在线演示地址:JavaScript Trailing Closure Toy Compiler

关于项目代码部分的详细解释可以阅读这篇文章:动手写一个简单的编译器:在 JavaScript 中使用 Swift 的尾闭包语法

关于这个小项目大家有什么想法和建议,欢迎在文章下面留言,我们一起交流一下。

3034 次点击
所在节点    分享创造
38 条回复
dawn009
2021-04-07 21:32:36 +08:00
楼主对“尾闭包”这种写法本身有没有什么评价?
我一直没有体会到它的好处,程序逻辑没有因此变得更清晰,也没有能让你少打几个字。
最常见的用法,是把 completion handler 放在那个位置写成“尾闭包”的形式,在视觉上暗示“会在函数执行完成后运行闭包内的代码”。但是,任何功能的闭包都可以放在那个位置,并不一定要是 completion handler,我完全可以用同样的方式调用一个“在函数开始之前运行”的闭包。
把 completion handler 放在尾部更像是一个习惯或是代码风格的问题。我觉得,把一个代码风格的设计直接内置到语言中是不是不太好。
dreamapplehappy
2021-04-07 21:44:13 +08:00
@dawn009 这是一个好问题,手动给你点个赞。说实话我还没有认真考虑过这个问题,还在刚开始的学习中。不过每种编程语言对一些相同的操作或多或少都会有自己的风格,感觉这很大程度上跟语言的创造者有关系。希望对此有了解的同学可以分享一下~
love
2021-04-07 22:01:57 +08:00
对 JS 没什么好处,但是 python 需要啊,python 写起来比 js 难受很多 lambda 只能单行也是一大原因
codehz
2021-04-07 22:07:36 +08:00
这不是 cps 风吗,以前某 livescript 都这样玩(
lujjjh
2021-04-07 23:14:02 +08:00
没写过 Swift,发现这个语法挺有趣的,我一开始也以为是 CPS 变换,仔细看发现不是。

搜了下,发现 Kotlin 也有类似的语法 https://kotlinlang.org/docs/lambdas.html#passing-trailing-lambdas

@dawn009 我的看法是,这种语法主要提供了一种定制 DSL 的能力,比如说你提到的 completion handler 就是一个例子,但也不限于 completion handler,比如可以用来定义 with (something) { ... } 的语法(只需要定义一个 with 函数)。表达力强的话甚至可以用来描述 UI,搜了下 SwiftUI 似乎就是这种玩法?

Kotlin 也有用 trailing lambda 定制 DSL 的例子 https://kotlinlang.org/docs/type-safe-builders.html
forvtest
2021-04-07 23:31:39 +08:00
我有点感觉 Swift 这么做是专门为了 SwiftUI 做准备的(参照推出尾闭包的时间节点)
irytu
2021-04-08 01:59:32 +08:00
表达力强吧很多时候,不过还是要看具体代码设计
dawn009
2021-04-08 02:56:37 +08:00
@lujjjh #5 这只是把大括号写在小括号里面还是外面的区别,好像没有改变表达力?

head({ ... })
head() { ... }
head { ... }
wipbssldo
2021-04-08 09:26:11 +08:00
我怎么感觉你这个转换后的不对劲
no1xsyzy
2021-04-08 10:02:32 +08:00
这难道不是从 Ruby 借鉴来的语法吗? do |params...|
body...
end

不过提醒一句,你这个语法在 js 里似乎某些情况下可能与现有语法冲突?
a(){} 似乎和 object 内定义函数的 a: function () {} 的语法糖是一样的,不知是否有区分。

@love Python 下大概可以用 PyMacro 的 f[] 宏代换一下(实际没试过:
@f[list(map(_, range(100)))]
def result(v):
  return v**2
wobuhuicode
2021-04-08 10:06:28 +08:00
写 swift 时候最不喜欢的语法之一。
iyeatse
2021-04-08 10:15:02 +08:00
@dawn009 如果用来描述 UI 的话,花括号里面的内容可能要超过几十行,这个情况下如果花括号结束之后还需要程序员记得关闭小括号那就很反人类了
lujjjh
2021-04-08 10:37:35 +08:00
@dawn009 看怎么理解表达力了,一般来说,语法越灵活,能定制出的 DSL 也越好用( head { ... } 显然比 head({ ... }) 或者 head(() => ...) 更简洁)。

不过我原文里的意思是,光有 trailing closure / lambda 这个特性是实现不了 html { head { ... } } 这种效果的。比如 head 里需要能够访问到 html 里实例化出的对象,才能把自己 append 进 html.children 。除了 trailing closure / lambda 之外,还需要结合其他特性才能定制出这种 DSL 。
abersheeran
2021-04-08 11:13:19 +08:00
@love Python 风格就是少用匿名函数。一个语言有一个语言的味道,拿 JS 硬套 Py,不难受就出鬼了。我就从来不拿 Py 那一套硬套 JS 。Py 、JS 我都用的挺爽的。
Jirajine
2021-04-08 11:31:47 +08:00
@lujjjh 应该能实现吧,有这个语法直接把 react 包装一下就可以了。
lujjjh
2021-04-08 11:38:44 +08:00
挽尊,给这个项目本身一点建议。这个项目用来学习写简单的编译器是没问题的,实用角度来看比较尴尬。

这个与其说是给 js 增加了 trailing closure 语法,不如说是搞了个能够 transpile 到 js 的 trailing closure language 。

如果你的想法是在 js 的基础上增加这个语法,那就得考虑很多问题:
1. 怎么兼容 js 现有的语法?
2. 这个语法有什么用,是不是还得配合实现 implicit return 、function builders 之类的特性才真的有用?
3. 怎么兼容 js 的工具链( language server 、eslint……)

如果还是想在 js 的基础上做一些文章而不是设计一个全新的语言,不妨考虑基于现有的语法创建新的语义。比如远古时代的 Wind.js[1] 在不修改 js 语法的基础上实现 async / await ( CPS 变换);再比如 Svelte[2] 用 label 表示 reactive declarations 。

[1]: https://github.com/JeffreyZhao/wind/blob/master/samples/async/browser/quick-start.html
[2]: https://svelte.dev/tutorial/reactive-declarations
lujjjh
2021-04-08 11:47:11 +08:00
@Jirajine 有 implicit return 的话可以用高阶函数实现一部分,但是要支持 children 有多个,像是
html {
head { ... }
body { ... }
}
的话,implicit return 做不到,还需要像是 Swift 的 function builders 或者 Kotlin 的 function literals with receivers + 可省略的 self. / this.。
Jirajine
2021-04-08 12:02:24 +08:00
@lujjjh 支持多个 children 的话,用 array literal 能不能做到?
html {
[head{},
body{}
]
}
还有一种思路是用对象成员.连起来
html{
head{}
.body{}
}
不过这样可能就要难看一点。

lambda 里支持 implicit return 肯定是基本的。可以参考 elm,我觉得比较接近。
lujjjh
2021-04-08 12:04:45 +08:00
@Jirajine 仔细想了想,单线程语言里可以做到,只不过会像 React hooks 的实现一样黑
lujjjh
2021-04-08 13:15:51 +08:00
@Jirajine 能接受数组的话为什么还需要 closure 呢?我写了个我认为比较接近的实现:

https://gist.github.com/lujjjh/1f10ed514191cd4d13e0057ed23ad6ed

https://jsbin.com/zarenil/edit?js,output

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/768848

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX