V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
dreamapplehappy
V2EX  ›  分享创造

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

  •  
  •   dreamapplehappy · 2021-04-07 21:10:54 +08:00 · 3281 次点击
    这是一个创建于 1355 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近业余时间在学习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 的尾闭包语法

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

    38 条回复    2021-04-12 01:10:28 +08:00
    dawn009
        1
    dawn009  
       2021-04-07 21:32:36 +08:00   ❤️ 3
    楼主对“尾闭包”这种写法本身有没有什么评价?
    我一直没有体会到它的好处,程序逻辑没有因此变得更清晰,也没有能让你少打几个字。
    最常见的用法,是把 completion handler 放在那个位置写成“尾闭包”的形式,在视觉上暗示“会在函数执行完成后运行闭包内的代码”。但是,任何功能的闭包都可以放在那个位置,并不一定要是 completion handler,我完全可以用同样的方式调用一个“在函数开始之前运行”的闭包。
    把 completion handler 放在尾部更像是一个习惯或是代码风格的问题。我觉得,把一个代码风格的设计直接内置到语言中是不是不太好。
    dreamapplehappy
        2
    dreamapplehappy  
    OP
       2021-04-07 21:44:13 +08:00
    @dawn009 这是一个好问题,手动给你点个赞。说实话我还没有认真考虑过这个问题,还在刚开始的学习中。不过每种编程语言对一些相同的操作或多或少都会有自己的风格,感觉这很大程度上跟语言的创造者有关系。希望对此有了解的同学可以分享一下~
    love
        3
    love  
       2021-04-07 22:01:57 +08:00
    对 JS 没什么好处,但是 python 需要啊,python 写起来比 js 难受很多 lambda 只能单行也是一大原因
    codehz
        4
    codehz  
       2021-04-07 22:07:36 +08:00 via Android
    这不是 cps 风吗,以前某 livescript 都这样玩(
    lujjjh
        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
    forvtest
        6
    forvtest  
       2021-04-07 23:31:39 +08:00
    我有点感觉 Swift 这么做是专门为了 SwiftUI 做准备的(参照推出尾闭包的时间节点)
    irytu
        7
    irytu  
       2021-04-08 01:59:32 +08:00 via iPhone
    表达力强吧很多时候,不过还是要看具体代码设计
    dawn009
        8
    dawn009  
       2021-04-08 02:56:37 +08:00
    @lujjjh #5 这只是把大括号写在小括号里面还是外面的区别,好像没有改变表达力?

    head({ ... })
    head() { ... }
    head { ... }
    wipbssldo
        9
    wipbssldo  
       2021-04-08 09:26:11 +08:00
    我怎么感觉你这个转换后的不对劲
    no1xsyzy
        10
    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
        11
    wobuhuicode  
       2021-04-08 10:06:28 +08:00   ❤️ 1
    写 swift 时候最不喜欢的语法之一。
    iyeatse
        12
    iyeatse  
       2021-04-08 10:15:02 +08:00 via iPhone
    @dawn009 如果用来描述 UI 的话,花括号里面的内容可能要超过几十行,这个情况下如果花括号结束之后还需要程序员记得关闭小括号那就很反人类了
    lujjjh
        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 。
    abersheeran
        14
    abersheeran  
       2021-04-08 11:13:19 +08:00
    @love Python 风格就是少用匿名函数。一个语言有一个语言的味道,拿 JS 硬套 Py,不难受就出鬼了。我就从来不拿 Py 那一套硬套 JS 。Py 、JS 我都用的挺爽的。
    Jirajine
        15
    Jirajine  
       2021-04-08 11:31:47 +08:00 via Android
    @lujjjh 应该能实现吧,有这个语法直接把 react 包装一下就可以了。
    lujjjh
        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
    lujjjh
        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.。
    Jirajine
        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,我觉得比较接近。
    lujjjh
        19
    lujjjh  
       2021-04-08 12:04:45 +08:00
    @Jirajine 仔细想了想,单线程语言里可以做到,只不过会像 React hooks 的实现一样黑
    lujjjh
        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
    no1xsyzy
        21
    no1xsyzy  
       2021-04-08 13:18:38 +08:00
    @Jirajine @lujjjh array literal 实现应当非常直观,大概拿 template string 写是这样
    html = (closure) => `<html>${closure().join('')}</html>`
    用 render 函数表达的话大概
    html = (closure) => h => h('html', {}, closure().map(render => render(h)))

    也可以添加判断 closure 返回值是单元素还是 array
    no1xsyzy
        22
    no1xsyzy  
       2021-04-08 13:24:10 +08:00
    @lujjjh 不需要全局,React (不管 hooks ) render 函数其实就是这种想法,你写得还是脏了
    只要把后面那个 closure call 出来的结果合并进去就成了
    lujjjh
        23
    lujjjh  
       2021-04-08 13:31:52 +08:00
    @no1xsyzy 你可能没有看全上下文。我当然知道 closure 返回数组的话很容易处理。

    我思考的问题是怎么避免手写数组,或者说怎么模拟 Swift 的 function builders,从而实现一个更简洁的 HTML DSL 。js 里引入全局的存储应该是唯一解。

    你可以看到我的代码里 L23-L28 在使用的时候完全没有手写数组。
    no1xsyzy
        24
    no1xsyzy  
       2021-04-08 14:21:07 +08:00
    @lujjjh 都已经动了编译器了,重新发明下宏就行了,不是什么大问题。
    lujjjh
        25
    lujjjh  
       2021-04-08 14:53:53 +08:00
    @no1xsyzy Talk is cheap,我的核心观点还是在 #16 。像 Svelte 那样动编译器跟动语法是两码事,动语法带来的问题太多了,工具链是问题,方言能不能被大众接受也是问题。React Hooks 实现得这么黑本质上也是为了在 js 的限制下设计出一套相对好用的 DSL,否则完全可以设计成 Vue Composition API 的样子,改动语法可能性就更多了。
    no1xsyzy
        26
    no1xsyzy  
       2021-04-08 15:11:20 +08:00
    @lujjjh 我是说主题已经动了编译器了,然后你还束手束脚; React hooks 这点问题也大,都已经 JSX 了其实没必要完全遵守 JS 的约束,有点又当又立的意思在。
    宏是 JS 很明显缺失的一环( JavaScript 是一个没能成为 Lisp 的 Lisp 方言),又其实有点大家都不敢抢先做的意思。
    lujjjh
        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
    CaffreySun
        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 不强制我们使用尾随闭包,只是给我们提供了更灵活的选择,
    我们可以完全不用它,我们也可以用它写出表达力更强、更简洁的代码。
    dreamapplehappy
        29
    dreamapplehappy  
    OP
       2021-04-09 00:03:55 +08:00
    @love 我今天搜索了一下 CPS 风格,感觉这两个应该还是有点区别的。对于 CPS 来说,传入的函数是用来获取原来函数的执行结果的,感觉应该是需要对这个结果做一些额外的操作;还有就是这个函数的位置应该也不需要是最后一个。而 Swift 的尾闭包要求是最后一个参数是一个函数,且一般可以不对之前函数的结果做什么操作。不知道我理解的对不对😂。从你的回复中学习到一些新的东西,谢谢回复。关于 CPS 我看的文章是这篇 [CPS 变换与 CPS 变换编译]( https://zhuanlan.zhihu.com/p/22721931)
    dreamapplehappy
        30
    dreamapplehappy  
    OP
       2021-04-09 00:11:58 +08:00
    @codehz 刚才回复错人了😂,回复给你楼上哪位同学了,尴尬;我粘贴过来吧
    ------
    我今天搜索了一下 CPS 风格,感觉这两个应该还是有点区别的。对于 CPS 来说,传入的函数是用来获取原来函数的执行结果的,感觉应该是需要对这个结果做一些额外的操作;还有就是这个函数的位置应该也不需要是最后一个。而 Swift 的尾闭包要求是最后一个参数是一个函数,且一般可以不对之前函数的结果做什么操作。不知道我理解的对不对😂。从你的回复中学习到一些新的东西,谢谢回复
    dreamapplehappy
        31
    dreamapplehappy  
    OP
       2021-04-09 00:14:40 +08:00
    @love 第一个回复,回复错了,是给你楼下的那位同学的,不好意思。
    ------
    对 JavaScript 来说,如果真的有这种语法的支持感觉也还不错。正好在 github 发现了一个类似的提议: https://github.com/samuelgoto/proposal-block-params
    dreamapplehappy
        32
    dreamapplehappy  
    OP
       2021-04-09 00:17:54 +08:00
    @wipbssldo 转换后哪个地方有问题,你说一下我看看😂
    dreamapplehappy
        33
    dreamapplehappy  
    OP
       2021-04-09 00:33:26 +08:00
    @no1xsyzy Ruby 没学习过,不是很了解。不过你说的跟在对象里面定义函数的语法有冲突,这个确实是的;如果真的要在实际中应用的话,可能要换种方式,或者检查一下上下文了。谢谢提醒。
    dreamapplehappy
        34
    dreamapplehappy  
    OP
       2021-04-09 00:34:34 +08:00
    @wobuhuicode 刚开始学习 SwiftUI 的时候确实有点不适应,不过慢慢也就习惯了😂
    dreamapplehappy
        35
    dreamapplehappy  
    OP
       2021-04-09 00:44:35 +08:00
    @lujjjh 首先学了个新的网络词语,“挽尊”,刚开始还不知道是啥意思😂。
    这个项目开始的时候确实是想练习一下写一个简单的编译器,也选择了一个我觉得还算有趣的练习方向。如果在实际中使用的话,确实要考虑很多的问题。比较好的解决方案是借助 Babel 进行语法的转换,可以参考: https://lihautan.com/creating-custom-javascript-syntax-with-babel/ 这篇文章。
    你的建议很有帮助,再次谢谢你的建议。
    codehz
        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 最后死掉了)
    wipbssldo
        37
    wipbssldo  
       2021-04-09 10:33:49 +08:00
    @dreamapplehappy 你这个示例贴反了吧?没看懂,你把一个尾随闭包的写法转换成一般闭包的写法?
    ericgui
        38
    ericgui  
       2021-04-12 01:10:28 +08:00
    https://juejin.cn/post/6844904199751221262

    这个文章讲的还可以。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   880 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 22:00 · PVG 06:00 · LAX 14:00 · JFK 17:00
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.