最近业余时间在学习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 的尾闭包语法
关于这个小项目大家有什么想法和建议,欢迎在文章下面留言,我们一起交流一下。
1
dawn009 2021-04-07 21:32:36 +08:00 2
楼主对“尾闭包”这种写法本身有没有什么评价?
我一直没有体会到它的好处,程序逻辑没有因此变得更清晰,也没有能让你少打几个字。 最常见的用法,是把 completion handler 放在那个位置写成“尾闭包”的形式,在视觉上暗示“会在函数执行完成后运行闭包内的代码”。但是,任何功能的闭包都可以放在那个位置,并不一定要是 completion handler,我完全可以用同样的方式调用一个“在函数开始之前运行”的闭包。 把 completion handler 放在尾部更像是一个习惯或是代码风格的问题。我觉得,把一个代码风格的设计直接内置到语言中是不是不太好。 |
2
dreamapplehappy OP @dawn009 这是一个好问题,手动给你点个赞。说实话我还没有认真考虑过这个问题,还在刚开始的学习中。不过每种编程语言对一些相同的操作或多或少都会有自己的风格,感觉这很大程度上跟语言的创造者有关系。希望对此有了解的同学可以分享一下~
|
3
love 2021-04-07 22:01:57 +08:00
对 JS 没什么好处,但是 python 需要啊,python 写起来比 js 难受很多 lambda 只能单行也是一大原因
|
4
codehz 2021-04-07 22:07:36 +08:00 via Android
这不是 cps 风吗,以前某 livescript 都这样玩(
|
5
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 |
6
forvtest 2021-04-07 23:31:39 +08:00
我有点感觉 Swift 这么做是专门为了 SwiftUI 做准备的(参照推出尾闭包的时间节点)
|
7
irytu 2021-04-08 01:59:32 +08:00 via iPhone
表达力强吧很多时候,不过还是要看具体代码设计
|
8
dawn009 2021-04-08 02:56:37 +08:00
|
9
wipbssldo 2021-04-08 09:26:11 +08:00
我怎么感觉你这个转换后的不对劲
|
10
no1xsyzy 2021-04-08 10:02:32 +08:00
|
11
wobuhuicode 2021-04-08 10:06:28 +08:00
写 swift 时候最不喜欢的语法之一。
|
12
iyeatse 2021-04-08 10:15:02 +08:00 via iPhone
@dawn009 如果用来描述 UI 的话,花括号里面的内容可能要超过几十行,这个情况下如果花括号结束之后还需要程序员记得关闭小括号那就很反人类了
|
13
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 。 |
14
abersheeran 2021-04-08 11:13:19 +08:00
@love Python 风格就是少用匿名函数。一个语言有一个语言的味道,拿 JS 硬套 Py,不难受就出鬼了。我就从来不拿 Py 那一套硬套 JS 。Py 、JS 我都用的挺爽的。
|
16
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 |
17
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.。 |
18
Jirajine 2021-04-08 12:02:24 +08:00 via Android
@lujjjh 支持多个 children 的话,用 array literal 能不能做到?
html { [head{}, body{} ] } 还有一种思路是用对象成员.连起来 html{ head{} .body{} } 不过这样可能就要难看一点。 lambda 里支持 implicit return 肯定是基本的。可以参考 elm,我觉得比较接近。 |
20
lujjjh 2021-04-08 13:15:51 +08:00
@Jirajine 能接受数组的话为什么还需要 closure 呢?我写了个我认为比较接近的实现:
https://gist.github.com/lujjjh/1f10ed514191cd4d13e0057ed23ad6ed https://jsbin.com/zarenil/edit?js,output |
21
no1xsyzy 2021-04-08 13:18:38 +08:00
|
22
no1xsyzy 2021-04-08 13:24:10 +08:00
@lujjjh 不需要全局,React (不管 hooks ) render 函数其实就是这种想法,你写得还是脏了
只要把后面那个 closure call 出来的结果合并进去就成了 |
23
lujjjh 2021-04-08 13:31:52 +08:00
@no1xsyzy 你可能没有看全上下文。我当然知道 closure 返回数组的话很容易处理。
我思考的问题是怎么避免手写数组,或者说怎么模拟 Swift 的 function builders,从而实现一个更简洁的 HTML DSL 。js 里引入全局的存储应该是唯一解。 你可以看到我的代码里 L23-L28 在使用的时候完全没有手写数组。 |
25
lujjjh 2021-04-08 14:53:53 +08:00
@no1xsyzy Talk is cheap,我的核心观点还是在 #16 。像 Svelte 那样动编译器跟动语法是两码事,动语法带来的问题太多了,工具链是问题,方言能不能被大众接受也是问题。React Hooks 实现得这么黑本质上也是为了在 js 的限制下设计出一套相对好用的 DSL,否则完全可以设计成 Vue Composition API 的样子,改动语法可能性就更多了。
|
26
no1xsyzy 2021-04-08 15:11:20 +08:00
@lujjjh 我是说主题已经动了编译器了,然后你还束手束脚; React hooks 这点问题也大,都已经 JSX 了其实没必要完全遵守 JS 的约束,有点又当又立的意思在。
宏是 JS 很明显缺失的一环( JavaScript 是一个没能成为 Lisp 的 Lisp 方言),又其实有点大家都不敢抢先做的意思。 |
27
lujjjh 2021-04-08 16:12:31 +08:00
@no1xsyzy 怎么感觉你对 js 这么恨铁不成钢[doge]。jsx 至少还算是 opt-in 的。
无意引起争论,给 ECMAScript 提 proposal 或者发明一种新语言都没啥问题。只不过在 #16 给出了点基于 js 扩展要考虑的问题和建议而已。至于后面那段实现很脏的代码,也是在跟 @Jirajine #18 探讨 js 实现类似 DSL 的可能性。 既然又回复了,我就再给这个「在 JavaScript 中使用 Swift 的尾闭包语法」的项目提点建议(如果楼主的本意是设计一门全新的语言请无视): * { b, c in ... } 这种语法毕竟是 Swift 的闭包语法,基于 js 扩展用类似 arrow function 的语法更具一致性。 * 可以在 babel 的基础上魔改,再写个插件,就可以在实际项目中使用了。 最后也分享个我搞的语言,欢迎交流: https://github.com/lujjjh/gates |
28
CaffreySun 2021-04-08 16:51:00 +08:00
@dawn009
尾随闭包不只是一种代码风格, 举个 SwiftUI 的🌰 ScrollView(.vertical) { VStack(spacing: 10) { ForEach(0..<100) { Text("Item \($0)") .font(.title) } } } ScrollView(.vertical, content: { VStack(spacing: 10, content: { ForEach(0..<100, content: { Text("Item \($0)") .font(.title) }) }) }) 两种写法都可以正常运行,但第一种明显更简洁。 Swift 不强制我们使用尾随闭包,只是给我们提供了更灵活的选择, 我们可以完全不用它,我们也可以用它写出表达力更强、更简洁的代码。 |
29
dreamapplehappy OP @love 我今天搜索了一下 CPS 风格,感觉这两个应该还是有点区别的。对于 CPS 来说,传入的函数是用来获取原来函数的执行结果的,感觉应该是需要对这个结果做一些额外的操作;还有就是这个函数的位置应该也不需要是最后一个。而 Swift 的尾闭包要求是最后一个参数是一个函数,且一般可以不对之前函数的结果做什么操作。不知道我理解的对不对😂。从你的回复中学习到一些新的东西,谢谢回复。关于 CPS 我看的文章是这篇 [CPS 变换与 CPS 变换编译]( https://zhuanlan.zhihu.com/p/22721931)
|
30
dreamapplehappy OP @codehz 刚才回复错人了😂,回复给你楼上哪位同学了,尴尬;我粘贴过来吧
------ 我今天搜索了一下 CPS 风格,感觉这两个应该还是有点区别的。对于 CPS 来说,传入的函数是用来获取原来函数的执行结果的,感觉应该是需要对这个结果做一些额外的操作;还有就是这个函数的位置应该也不需要是最后一个。而 Swift 的尾闭包要求是最后一个参数是一个函数,且一般可以不对之前函数的结果做什么操作。不知道我理解的对不对😂。从你的回复中学习到一些新的东西,谢谢回复 |
31
dreamapplehappy OP @love 第一个回复,回复错了,是给你楼下的那位同学的,不好意思。
------ 对 JavaScript 来说,如果真的有这种语法的支持感觉也还不错。正好在 github 发现了一个类似的提议: https://github.com/samuelgoto/proposal-block-params |
32
dreamapplehappy OP @wipbssldo 转换后哪个地方有问题,你说一下我看看😂
|
33
dreamapplehappy OP @no1xsyzy Ruby 没学习过,不是很了解。不过你说的跟在对象里面定义函数的语法有冲突,这个确实是的;如果真的要在实际中应用的话,可能要换种方式,或者检查一下上下文了。谢谢提醒。
|
34
dreamapplehappy OP @wobuhuicode 刚开始学习 SwiftUI 的时候确实有点不适应,不过慢慢也就习惯了😂
|
35
dreamapplehappy OP @lujjjh 首先学了个新的网络词语,“挽尊”,刚开始还不知道是啥意思😂。
这个项目开始的时候确实是想练习一下写一个简单的编译器,也选择了一个我觉得还算有趣的练习方向。如果在实际中使用的话,确实要考虑很多的问题。比较好的解决方案是借助 Babel 进行语法的转换,可以参考: https://lihautan.com/creating-custom-javascript-syntax-with-babel/ 这篇文章。 你的建议很有帮助,再次谢谢你的建议。 |
36
codehz 2021-04-09 01:44:45 +08:00
@dreamapplehappy 但是这并不是我的原意,再加一个风就是为了避免歧义(因为这本来是编译原理的一种优化方法的内部表示,而并非由用户直接使用的语法格式)
我那句话的重点是 livescript 的转换效果 b,c <- a 1 2 console.log b c 编译成 a(1, 2, function(b, c){ return console.log(b(c)); }); 而你例子里的写法在 livescript 可以写成 b, c <-! a 1 "hello" d! do 防吞空格<~ d d 1 "hello" do 防吞空格<- d 1 "hello" e, f <-! d 1 "hello" g! (所以玩 livescript 最后死掉了) |
37
wipbssldo 2021-04-09 10:33:49 +08:00
@dreamapplehappy 你这个示例贴反了吧?没看懂,你把一个尾随闭包的写法转换成一般闭包的写法?
|
38
ericgui 2021-04-12 01:10:28 +08:00
|