好吧,大概两个月没发东西了……从现状来看,我大概活着…… 其实要是以前的话大概会没事发个现状总结给死党看,然则现在她在这儿没存在感……

最近发生的比较 RP 的事情是今天知道自己在松本行弘访谈问题征集里中奖了! 大概是因为这辈子的 RP Point 全都积攒到了一些特殊事件上,从小到大从来没有中过什么奖,包括各种再来一瓶、再来一根、再来一袋、30% 送卤蛋什么的都是各种没中过……所以这次真心是这辈子第一次中奖2333 奖品是松本行弘签名的《松本行弘的程序世界》,虽然这书我早有一本读完了的XD

签名书照

我提问的问题是这样的:

设计语言的时候往往需要在很多语言特性中进行取舍,比如多继承和 Mixin,比如基于类还是基于原型等等。对 Matz 先生来说,在设计 Ruby 的时候感到最难以取舍的、考察最深入的语言特性是什么?有没有哪个特性会让您觉得“啊,当年若是选择那么做而不是这么做,虽然不会是现在的 Ruby,但应该也不错”?

之所以会问这个问题,是因为自己的确正在想实现一个语言练练手。 在考察各种语言特性的时候(当然程序语言并不是特性的堆积),总是觉得大多数时候这些特性没有绝对的对与错,而更多的是根据语言自身的特点和目的来进行的取舍和妥协。 显然我在很多这类的地方纠结了很久很久,所以想趁机请教下 Matz 大神,看他当年在这种事情上是怎么纠结的 :) 本来觉得这问题挺个人兴趣的,被选上的确出乎意料,可惜不能当场去听回答了XD

更新: Matz 的回答在这里。 Matz 首先强调最重要的选择是静态语言和动态语言,这个略出乎我意料,不过应该是我没考虑好…… “当我想设计一种编程语言作为自己的工具来用的时候,我就觉得还是动态语言实际用起来比较好用”这点的确有同感。 Matz 还强调了 Mixin, 这个在预料之中,在那个年代选择这种设计确实是比较有挑战性的。 Matz 还感慨了下从 Perl 里拿的有点过了,这个的确是很明显的,那些 Perl 里很 magic 的 $XX 就都拿过来了,要说方便是方便,但也太 magic 了点。 这个也的确值得警醒,不要从要参考的语言里拿太多XD

顺便感慨句,Matz 太懂中国了233

其实本来最近就想就自己对想实现的 Toy language 的各种考量写个 Note 的(记性奇烂无比,已发生过几个月前考虑过的问题不得不再考虑一次的窘境,所以干脆记下来的好),既然挨上好事了,就先占个位子慢慢来…… 当然读者必然假定是未来的自己了,如果在读这段文字的恰好是未来的我,麻烦从 金馆长脸/姚明脸/兔斯基以头抢砖 表情中选一个,现在的我推荐姚明脸……

最后一次更新是 2012/11/14. 显然最近这事的优先级不高,下次更新不排除隔周啥的2333


展开所有项目


Meta Questions

为什么大多数问题的回答都是“因为我懒……”?

  • 因为我现在懒……
  • 事实是我急着睡觉,而大部分问题我嫌敲起来麻烦还没整理到这里……

为什么问题明显不足,有些问题明显标题都没起好?

因为我懒……

为何起名为 Aqua?

一开始有想法的时候其实想起名为 Jade 的,因为曾经在绯月上看过 haibara 大他们的讨论,说过“既然日本有 Ruby,中国就叫 Jade 吧XD”,不过 Jade 已经是比较知名的语言了所以作罢。

后来一直在用的称呼是 Lotus, 因为打算之后在其上构建一套叫 Dark Crow 的系统,熟悉加速世界的应该能对这个典故一目了然…… 缺点很明显,大名鼎鼎的 Lotus Note 已经占掉了 Lotus Script 这个名号,所以在真正开始实现之前只是以此为代名。 (P.S.:最近刚听说 IBM 要把 Lotus 撤了?)

Matz 在他的程序世界一书中曾强调名字的重要性,王培在他的通用人工智能课程上也强调为组合概念起名字的重要性,这方面我一直深感赞同。 没有名字的东西是没法深入讨论的,只有起了名字,才能脱离原有的表面的理解,去探究更多细致的特性。 连代号都没有的话,心里想“这个语言要满足这样的特点”的时候,就只能“这个”“这个”的叫,也怪别扭的。

现在这个名字 Aqua 是某天晚上决心开始动手时紧急起的,一方面比较简单易记,一方面似乎没有太出名的重名语言。 水本身既沉稳安逸,也有灵活多变的一面,也符合我希望这个语言满足的特点。 此外 Aqua 也是黑暗星云的成员之一,而我的星座也是瓶子。 做为毫无艺术细胞的我,这样随便撞脑门起了这样的名字,似乎也算是合格了吧。

Implement

为何选择 VM 而不是解释或编译?

从个人感受而言,我是认为真正的程序语言是解释的,编译是根据可以静态推断的信息进行的一种优化。 不过这里的语言,应该是指脱离了纯文本语法层面的语言,至少在 AST 层面上的。

话虽这么说,设计程序语言这种事情就是要做各种 trade off 的…… 从现阶段来说,使用 VM 实现我能想象到的好处有:

  1. 我可以在 VM 这边玩很长时间而不用去考虑 parser 和 bytecode compiler 的实现: 前端那边因为语法未定而变化很快,而 VM 这边确定主要特性后基本可以动手了,也不必担心前端变化带来的影响太大。
  2. 移植方便,parser 不移植都可以,只要用 aqua 自己写一个 parser 就可以了,这一点后头还会提到。
  3. 事实上,上面都是骗人的……其实是以前我在脑补 continuation 的实现的时候基于 VM 的方式几乎是立刻想到了,但是基于 AST 解释的方式卡在了奇怪的地方,然后我恰好选择在这个时候第一次动手了……虽然后来我又不打算实现 continuation 了233333333

为何选择使用 Python 实现?

基于高级语言实现的最大优势是站的高看的远,很多时候不必在细节上浪费功夫。 尤其是现在还在设计阶段,大把大把的变化,放在 Python 里能省不少笔墨。

基于高级语言的最大缺点就是性能。 对于设计阶段的 Aqua 来说,开发快速原型并迅速更改,要比效率问题重要太多了。 另一方面,PyPy 在设计动态语言方面表现突出,具体可以看他们的 paper.

另一个问题是可移植性。Python 还算平台众多。而且最关键的是这只是个原型设计,重写在必要时是会提上议程的,前提是用户不再只是我一个,这个有点不现实233

嘛,其实我又在骗人了……我只是不想考虑 GC 而已233333 虽然从现在的实现来看,如果考虑 GC 的确会很折寿…… 这至少说明使用 Python 减少了我因为实现难度而放弃特性的可能。

Syntax

为何选择接近 Python 的语法?

首先,接近 Python 显然是因为我很喜欢 Python 风格的代码,清晰易读废话少:)

至于为什么接近到很多地方几乎完全兼容,其实主要是因为我有个很233的想法,就是希望 parser/bytecode compiler 既是合法的 Python(甚至 RPython) 代码,也是合法的 Aqua 代码。 这样的话就可以实现在 VM 层之上的 bootstrap 了。 而且这个 parser 也可以直接拿到 Aqua 里当作 eval 的一部分,也可以在没有 Aqua 的机器上使用 Python 直接为 Aqua 输出字节码,总之好处多多。

至于 VM 端,如果可能也是 Python/Aqua 通吃的话那也最好,但考虑到还有运行时,另外 class 等的设计可能和 Python 有很大出入,大概不是很指望了。

为何保留 Python 的缩进语法?

TODO: 整理中

因为希望能让 Python 来启动 bootstrap, 自然要保留缩进XD 不过即使不考虑这个,我也会希望保留这点。

处理缩进、调整 Tab 和空格、为了防止对错位置而折叠代码并提供参考线等功能明显应该是编辑器的工作。

使用缩进规定语法的语言也有不少了,除了 Python, Haskell, F# 和 CoffeeScript 等语言也在用,我最喜欢的数据存储格式 YAML (这才是给人读的的格式!) 也在用。

实际上,我认为语句中的大括号和表达式中的小括号一样,应该是一种类似调整优先级、消除歧义的功能。 大括号在需要的时候(缩进表达不清楚)可以加上,在不需要的时候(靠缩进就足够清楚)完全可以去掉。

对于缩进的坏处,我见过的最靠谱的驳斥来自王垠。 王垠认为在使用 Layout Syntax 时,由于对齐问题,导致犯错成本太低。 在移动代码块时,重新缩进变得不得不立刻操作。 而其优点,譬如代码更短、更少的括号更清晰等,又不甚明显。

Parse 不方便。

Layout Syntax 的另一个问题是嵌入其它语言中时很不方便,比如 Web 页面的模板。 不过这方面 Python 自己就有很多示范。 显然这个问题是能解决的,虽然大多嵌入的语言都做了一定的修改来适应。 我觉得这个问题还是个“大括号在缩进表达不清楚时可以加上”的问题。 只不过 Python 没有去考虑这个问题,导致模板语言设计者自己去解决了。

总的来说,缩进本身自然是好的。 而将缩进强制规定到语法层面,和很多其它选择一样是一种 trade off. 而我认为这种交易是值得的。

Function & Block

有关变量作用域的判定

在各个语言中随便试验了一下与下面代码类似功能的代码:

def test():
    x = 1               # x1
    def f():
        def g():
            print(x)    # 哪一个 x?
        g()             # 是否找的到 x?
        x = 2           # x2
        g()
    f()
test()
  1. g 中的 x 指 x1: lua 和 go —— 不过 golang 在有不使用的变量的时候是无法通过编译的,所以要在比如 x2 后头 println 一下 x 什么的;

  2. g 中的 x 指 x2: python, scheme(使用define) 和 javascript(nodejs). Python 在第一次调用 g 时会因为 x2 尚未赋值而抛出异常。Scheme 这边,guile 的行为类似 python, racket 则类似 javascript;

  3. 动态作用域: perl(local);

  4. 不需要考虑这类问题: lisp 等,因为let的作用域很明确; C 等也不需要考虑,因为没有嵌套函数;

  5. ruby(lambda/proc) 和 coffee 有点特别。coffee 中类似这样写的话,x2 处的x = 2实际上指的是给 x1 赋值为 2,所以 g 中的 x 指 x1, 但运行显示的是12. Ruby 的 lambda/proc 和 coffee 这样设计应该是希望尽量避免同名覆盖,不过也有令人困惑的地方:对 ruby 来说,还可以用def来屏蔽作用域;但对于 coffee,至少我直到看到翻译后的 javascript 代码前确实没有预料到这里的x = 2竟然是那样的作用,这样反而意外地修改了外部的 x 的值,尽管在需要闭包的地方的确会方便的多。

Macro?

因为我懒……

模式匹配

  1. 模式匹配的价值
  2. 有序/无序匹配

Exception

关于异常和返回值

这个问题几乎是 Go 语言掀到风头浪尖的。 Go 语言中取消了异常,转而使用 C 风格的返回值进行错误处理(尽管 Go 中仍然保留了 panic 用于从深层调用中跳出,但一般要求限制在包内部)。 Go 社区对此已有过热烈的争论 与此相关的文章也有很多。我个人最赞同这篇文章的观点,即:

  1. 无论是使用异常还是使用返回值,人都是可能犯错的;
  2. 一般情况下,使用异常相对而言更安全;
  3. 但对严肃的、关键的、被严格审查的代码而言(尤其系统编程中),使用返回值的代码更容易控制。

这是因为人天性是懒惰的,因此在不严肃的编程中(尤其脚本编程中),忘记处理返回值和忘记处理异常都是可能的。 相比而言,忘记处理异常所产生的错误更为明显。 但是当需要仔细审查的时候,随处都有可能抛出异常的代码的控制流显然更加难以琢磨。 对于以 C 语言为主要对手(虽然现实是更多的脚本小子更有转行的看法XD)的 Go 语言而言,选择使用返回值处理错误完全可以理解。 因为对严肃的应用,非确定性的异常抛出是灾难性的。 但对于不严肃到那种程度的场合,使用异常相对而言是要痛快些,而且有些时候主线也更清晰。 Aqua 的定位还是一个 Script Language,所以我还是更希望保留异常系统的。

对 finally 和异常的看法

关于 finally 代码块的含义,一般被认为是“保证某段代码执行结束时一定会调用的代码”。 其作用一般是清洁工作,保证资源在任何情况下都被正常释放等。 但当 finally 代码块中包含流程跳转时——特别地,可能抛出异常时——其含义变得不甚符合直觉了。

  1. Python 在 finally 中可以使用 return 和 break, 也可以抛出异常。

    如果使用了 return, 进入 finally 前试图返回的值会被这里替换。

    如果进入 finally 前正在抛出异常,该异常抛出的行为会因为 finally 中的 return/break 而中断,也会被 finally 中抛出的异常所覆盖,这些通常不是想要的结果(Java 和 Ruby 也有相同的问题)。

    特殊地,Python 不允许在 finally 块中使用 continue, 称为是 CPython 的实现问题。 猜测大概是因为 CPython 中 continue 在字节码层面上被直接实现为 GOTO 的缘故(对应地,break 有单独的字节码,并且所需信息在 SETUP_LOOP 中已经存储)。

  2. Golang 中倾向使用错误返回值,但也可以使用 panic 和 recover 实现非局部跳转。 Golang 中的 defer 从某些意义上部分近似于 finally.

    Golang 中的 defer 可以直接修改返回值。 defer 后跟着的是函数,因此不需要考虑 return/break/continue 等。

    Golang 总是保证所有 defer 执行完毕,就算是 defer 中产生 panic 也不例外——此时 golang 会选择中断这个 defer 去执行下一个 defer,并且将原先打算抛出的 panic 信息替换为此次 defer 发生 panic 的信息(尽管 golang 应该是记得之前 panic 的信息的,因为如果一直不 recover, 最后会打印所有 panic 的信息)。

  3. Dlang 是比较特别的语言,它同时包括了 finally 和 defer(scope).

    Dlang 要求 finally 和 defer 中不能有 return/break/continue 等,也不允许使用 goto 再入,defer 中甚至要求不得 throw——尽管如此,它实际上不可能保证不在 defer 中抛出异常。

    Dlang 的 defer 中抛出异常和 golang 一样,要保证所有 defer 按 LIFO 的顺序执行完毕,但是 defer 中出现的异常会靠 Throwable.next 串起来,最后抛出的异常仍然是第一个发生的异常——尽管有忘记处理 .next 的风险。 Finally 中抛出的异常也会这样串在后面。

    (P.S.: Dlang 比较奇特的是 1/0 运行时直接浮点数例外,1.0/0.0 运行时是 inf,都不是异常……估计前者是类C,为了效率;后者是浮点数精度问题。)

实际上在有异常的系统中,有些地方是不应该出现异常的,对于 finally/defer/析构函数等,它们应该做的是收尾的操作,本不应抛出异常。实际上,这种地方抛出异常应当视为错误。而且如上面所说的,在抛出异常的过程中经过 finally 的时候如果发生异常,其行为可能是超出预期的。 同样的,捕获异常的 catch/except 等中实际上也是不应该发生异常的——我能想到的范围中,只有将异常重新包装并继续向上传播是符合预期的行为,在捕获异常的过程中意外发生的异常本身也应该是错误



comments powered by Disqus