Home > misc > 一门天生就能损害人视力的语言 -> Erlang?

一门天生就能损害人视力的语言 -> Erlang?

December 1st, 2008 :: jackyz

这次去 softcon 2008 讲了一个介绍 Erlang 的话题,也顺便见识了一回 “网络传播” 的厉害。在迅速的(紧张的)讲完准备得过于充分的 slide 之后,本来也是午饭时间,所以有了一个较为充裕的提问时间。于是获得了其中一个这样的听众提问,原话已经记不清楚了,大致是:

……
据我所知,Erlang 有损害视力的问题,那么你觉得,目前其他的语言要怎样才能具备 Erlang 这样的并发特性?
……

让我难以理解的是,明明就是从来就没有用过这门语言的同学,说起话来,却好像已经是因为这门语法 “损害视力” 而即将弃之离开一样,对其 “刚一见面” 就 “根深蒂固” 的有了这样恶劣的印象。这让我觉得,之前被我一笑置之的论战,似乎后果还真的有点严重。

这件事说来也很吊诡——一门尚未为大众所了解的语言,俨然就已经带上了 “损害视力” 的恶名,而且还广为传播,这可如何是好?所谓言之凿凿,又道三人成虎,如果仅仅是因为这样的谬种流传(好吧,错对我们姑且不论,至少那个说法也只能是代表作者他自己个人意见的说法而已吧,为什么毫无辨别的就全盘接收了呢?至少自己动手实验一下再做结论也不迟嘛。),就让大家错失一项具有压倒性优势的并发技术,那会使得中国的软件从业人员因此而白白失掉多少“跻身高端产业”(说白了就是提升工资)的机会呢?

好吧,亡羊补牢,为时未晚。且让我来补充 “另外一种代表作者他自己个人意见的说法” 吧。

语法上,也不是很罕见的 -> 符号,好吧,我得承认,有的程序员看着会觉得很扎眼。拿它当作函数体的标记,确实并不多见。不过,既然 Erlang 是建立在模式匹配基础之上的语言,那么在一个模式匹配上了之后用 -> 指向它的分支代码入口,也不能算是什么丑陋的设计。毕竟 Erlang 并不崇尚“单一入口,单一出口”,用 -> 来标记每个分支,不也挺清晰的么。

说到眼花缭乱的模式匹配,初到 Erlang 的世界,确实难以产生好感。可是,这个东西也太 TM 好用了(请原谅我的粗鄙)。根据数据的模式(值)各自导向不同的分支,根据某位置上值的不同约束条件导向不同的分支,一个语句就能将 N 个分别位于不同位置的值提取出来,这在“非模式匹配”的语言之中确实太不常见了。以至于刚开始,不习惯的同学会认为这很丑陋。可是,当你用过这些设施来对消息/协议/数据包进行过哪怕只有一次的实践编码之后,你就会发现,这些语法设施是多么的简单和优美。同时你也会痛苦的发现此前自己处理这些业务的代码是多么恐怖,打死你我也不相信有人会愿意再回到过去。

List Comprehensions 和模式匹配一样,也不是 Erlang 的发明。如果有人对于在 FP 世界中最为常见的抽象数据数据语法糖在实践了一段之后仍然感到十分过敏的话(实际上,使用相同语义的 lists 库的包装函数,会更加适合命令式编程风格的眼睛),那么我只能很遗憾地摇摇头,对他说:还是回火星去吧,FP 真的不适合你。

用 Erlang 编程,稍微上点规模的程序,总会有那么几个复杂点的数据结构,遇到的各种括号的机会确实多得很,对于这一点,我也挺烦它的。在命令式语言(尤其是OOP语言)之中,可能就没有这个括号较多的问题(因为数据和操作被放到了一起)。不过,有问题就有解决办法,早在 emacs 时代(我本来不想拉它出来吓唬用惯了 IDE 的小朋友们)发明 Erlang 的那帮程序员们就已经整好了代码格式化的功能。再复杂的数据结构一个快捷键就立刻乖乖按照“显而易见”的格式列队站好,一层层乖乖地缩进(好消息是,两大 Erlang IDE 都已经具备了这个功能),再也用不着你自己去费眼配对。说到这儿,我想起了大学时代,我们的 pascal 老师在点评我们的程序之前,总要先自己动手做一遍格式缩进,一边敲键盘一边还语重心长的对我们说:“我们编程序的,多多少少还是要讲一点风格”,那才是真正的体力活(辛苦了,老师!)。

至于 record 的更新语法 R2 = R1#record{f1=Val1} 我在 erlang-china 的 group 里面已经详细地回答过,实在是懒得再写了,直接转贴如下:

laona:

这是书中的例子:

2> X=#todo{}.
#todo{status = reminder,who = joe,text = undefined}

3> X1 = #todo{status=urgent, text=”Fix errata in book”}.
#todo{status = urgent,who = joe,text = “Fix errata in book”}

4> X2 = X1#todo{status=done}.
#todo{status = done,who = joe,text = “Fix errata in book”}

“2>”和“3>”中的算符“#”意思单纯,容易理解。别扭的语法是“4>”。

“X2 = X1#todo{status=done}” 在此表示的意思,似乎是:X2的值约束为类型是todo的记录X1,并且只修改“status=done”这一项值。这种语法形式,的确晦涩难懂,并且,没有多少实用价值,甚至会有歧义。

“3>”已经确定了 X1 是 todo类型的记录,并且已经有了不可更改的值,却在“4>”中更改X1的定值,是不是有些自相矛盾?

jackyz:

我是读到这两条的时候,回头再想,才觉得这个语法是”make sense”的。
1. Erlang 的变量是单次绑定的,绑定之后就不能再绑定。
2. Erlang 没有赋值,只有模式匹配。模式匹配对未绑定变量的处理就是将其绑上值。
以此为基础,我这么理解这两个语句:

3> X1 = #todo{status=urgent, text=”Fix errata in book”}.
#todo{status = urgent,who = joe,text = “Fix errata in book”}
模式匹配符右侧 #todo{status=urgent, text=”Fix errata in book”} 语句的执行结果是一个 record 值(一个 tuple 值),模式匹配符左侧的 X1 为未绑定变量,这个匹配的结果就是将 X1 绑定为右侧的值。

4> X2 = X1#todo{status=done}.
#todo{status = done,who = joe,text = “Fix errata in book”}
模式匹配符右侧 X1#todo{status=done} 语句用 record X1 和 atom done 以 status 位置对应 done 值的方式 “合成” 了一个 record 值(一个新的 tuple 值),模式匹配符左侧的 X2 也为未绑定变量,这个匹配的结果也是将 X2 绑定为右侧的值。

这种对于 record transfer 操作的语法糖,刚开始用时,我也很不习惯,但在用了一段之后,我开始觉得很省心。因为,在 Erlang 编程中这是极为常用的操作,相比之下,其他在其他语言下更常用的操作则变得很少(比如,单独获取某个字段的值)。模式匹配和 record 语法的组合使得这样的工作能够一句话搞定。非常之简洁。其他比较常用的模式匹配和 record 组合还有:

%% 一个语句,提取 record 中的多个变量
#todo{status=Status, text=Text} = X2.

%% 直接对 record 的值进行模式匹配
case
#todo{status=done} ->

总结一下,上面说的是 “这些怪异的语法” 在 Erlang 的实践编程之中有其独到的用处,而且会上瘾。当然了,你完全也有不喜欢的自由,重要的是,我们写程序又不是搞“泯注集中制”,还是要把正反两个方面都讲清楚,讲透彻了,然后再把选择的权力交还给你。

至于说 Erlang 的 IDE 不成熟(虽说我已经习惯了 emacs 环境,并且开始喜欢),注释语法不 modern (虽说快捷键也很好用),不支持 unicode 字符串数据类型(虽说当成 binary 来处理更加合我的胃口),说的都在理,我也很同意社区对此作出改进,而且,更进一步,我还认为面市 20 多年 Erlang 的语法确实也太 old fashion,太不 sexy 了。不过,我抓破了头皮也没想出这些和 “损害视力” 的指责到底在哪儿能发生联系?

如果说 “损害视力” 的逻辑论证来自于 “我不喜欢” 或者 “我很牛逼” ,那挺好,明白说清楚就好。追求精神境界,大家都支持而且理解。既然不是考公务员写“申论”,又何必论上一大篇呢(害得我也跟着打了大半天的字,累得够呛)。

misc

  1. zefeng
    December 1st, 2008 at 16:46 | #1

    这个问题以前也好像在javaeye那里见过,当时只是因为它是有关Erlang的文章才进去看看,没过两分钟,就不往下看了.”损害人的视力”??.写代码时,手指是跟着思维轻快的敲键盘,哪有心思照顾视力啊.如果这样也说得通,那汇编不是更加损害视力了.学生的我也认为这个完全是胡扯.暂时精神上支持Erlang,等12月忙完,再实践实践,研究研究.

  2. December 1st, 2008 at 21:02 | #2

    呵呵,那哥们列出的编译器代码,初学者一看当然觉得头大。实际上大部分框架都使用OTP来开发,代码还是清晰易懂的。

  3. sw2wolf
    December 3rd, 2008 at 14:13 | #3

    erlang用习惯了还真容易上瘾, 比如:模式匹配就让代码少很多if, else等。

  4. xvyu
    December 3rd, 2008 at 16:14 | #4

    请教一个问题
    假设需要一个函数不断接收消息并修改相关变量的值,应该怎么写

    我现在是类似这样,例子有点极端

    loop(X1,X2,X3,……,X100)->
    receive
    {update,1}->
    loop(X1+1,X2,X3,……,X100);
    {update,2}->
    loop(X1,X2+1,X3,……,X100);
    ……
    {update,100}->
    loop(X1,X2,X3,……,X100+1)
    end.

    非常冗余,各位怎么处理呢

    再问个ide的问题,我目前用的是eclipse+erlide
    但是在0.394以后的版本,包括今天更新的0.42,都没有语法高亮了,不清楚怎么回事
    还有没有别的IDE推荐呢,想试一下

  5. Magicloud
    December 4th, 2008 at 15:49 | #5

    所谓“损害视力”,原因很简单,fp中没有变量,于是会有用 函数调用、表达式、列表领悟 等写在一行的情况(比如javaeye上那贴的那行代码),没有好的格式化,没有书写者自己的处理,说它损害视力不为过。
    不过,如果因为书写者不肯美化而认定语言是坏的,那非fp语言中的如下代码:
    var1 = fun1 ();
    var2 = fun2 (var1);
    var3 = fun3 (var2);
    写成:
    var3 = fun3 (fun2 (fun1 ()))
    也实在是损害视力。

    但是,这个问题也有其他解决方法,比如haskell就提供了一些难于看懂但确实比较美观的“省略”语法($、.等)。

  6. anonymous
    December 4th, 2008 at 18:51 | #6

    很显然,能指望在一个java的网站上,看到说其它东西好的更多言论吗?
    说损害视力是不负责任的,自己没试过就相信这种说法,唉

  7. December 4th, 2008 at 23:13 | #7

    @xvyu,你的数据结构,也就是参数之中的那些 X1,X2,X3,……,X100 是否适合考虑包装成一个 record 呢?

    -record(state, {a, b, c, d}).

    loop(State)->
    receive
    {update,a}->
    loop(State#state{a=State#state.a + 1});
    {update,b}->
    loop(State#state{b=State#state.b + 1}});
    ……
    {update,d}->
    loop(State#state{d=State#state.d + 1}})
    end.

    更进一步,如果你的对应关系如此明确的话,record 仍是 tuple ,你还有 nth() 函数可以用。

  8. December 4th, 2008 at 23:15 | #8

    @xvyu,如果想更通用,你还有 proplist 数据类型可以用。

  9. xvyu
    December 5th, 2008 at 16:29 | #9

    多谢楼上的回答

    我写的代码只是举例,实际使用并不是这个情形

    包装成record我也考虑过,但有另外一个担忧。每次更改一个属性,都要复制整个record,在上面的例子,就是100倍的时间和空间开销。

  10. jackyz
    December 6th, 2008 at 09:30 | #10

    @xvyu,这里是一个设计决策。

    A。为时间和空间上的性能考虑,允许修改内存,但会引入 “顺序瓶颈” 。
    B。不允许修改内存,用更 “浪费” 的方式,但不会引入 “顺序瓶颈” 。

    你已经知道 Erlang 在这里的设计决策是方案 B。

    另外,语义和语句是两回事,虽然没有看到确切的代码,但在实现上 BEAM 的底层并不一定会排斥 “修改内存” 的实现手段。比如,若能通过语义分析排除 “共享” 的情况,完全可以很安全的使用 “修改内存” 的方式来实现上述语义。

    使用“高级语言”时,如果总是忍不住在揣度它的底层实现,并因此而担心“时间和空间开销”。那还不如自己动手,写段测试代码来一探究竟。若有所收获,可别忘了与社区共享哦。 :)

  11. xvyu
    December 7th, 2008 at 12:08 | #11

    测试了一下,差别还是蛮大的

    -record(obj,{x1,x2,x3,x4,x5}).

    start()->
    N = 1000000,
    Obj = #obj{x1=N,x2=0,x3=0,x4=0,x5=0},
    T0 = now(),
    loop1(N,Obj),
    T1 = now(),
    loop2(N,0,0,0,0,0),
    T2 = now(),
    loop3(N,0),
    T3 = now(),
    io:format(”T:~p,~p,~p~n”,[timer:now_diff(T1,T0),timer:now_diff(T2,T1),timer:now_diff(T3,T2)]).

    loop1(0,_)->
    ok;
    loop1(N,Obj)->
    loop1(N-1,Obj#obj{x1=N}).

    loop2(0,_,_,_,_,_)->
    ok;
    loop2(N,X1,X2,X3,X4,X5)->
    loop2(N-1,N,X2,X3,X4,X5).

    loop3(0,_)->
    ok;
    loop3(N,X1)->
    loop3(N-1,N).

    结果如下:
    T:172000,47000,46000

    可以看出复制数据的开销确实不小,增加函数参数也会带来一定开销,但相对小很多。

  12. jackyz
    December 7th, 2008 at 15:02 | #12

    测试写得很不错。 :D

    对于这个结果,我的看法不同。我们写程序时,性能不应该被预先排在第一位,就算单单只考虑性能,问题也往往会很复杂。

    record 的 copy 毕竟也是 copy,肯定会有消耗。它比增加变量要复杂,才慢上 3 倍,已属不错(我估计,如果 record 更复杂,会慢得更多)。不过执行 1 次消耗 0.172 毫秒,在性能上,完全处在可以接受的范围。况且,这样的语法,还有易于理解,便于维护的好处(比如,要加一个字段)。

    如果对采用 record 的程序改用 “增加变量” 的方式来改写,且不谈对程序的可读性会带来多大影响,单从性能而言,我认为对整体性能提升的贡献也可能极为有限(比如,毫秒级),而通过 profiler 你可能会发现严重得多的性能瓶颈(比如,逻辑上的问题,IO,流程等等),修正这些问题往往会带来大得多的性能提升(比如,解决了一个瓶颈,一下子快上个几十上百倍完全是有可能的)。

  13. xvyu
    December 7th, 2008 at 17:29 | #13

    我的程序里要修改的状态变量是比较复杂的record,每个大概都有20个属性,所以我才有性能上的忧虑,当然变量数量没有100个这么多。

    不过,变量复制的代价是可以接受的,多谢你的回复。

    你们翻译的《Programming Erlang》出版得太晚了,英文电子版我已经看了好几遍,不然一定买。

    还有就是感觉那本书太基础了些,erlang领域有没有深一些的书籍呢。

  14. jackyz
    December 8th, 2008 at 13:00 | #14

    @xvyu,这么说吧,其实这个问题我感觉和 record 到底费不费时间关系不大,反而更象是一个 “程序调优的最佳实践” 问题。写程序的时候应该关注性能,但仍然有一个 “什么时机关注” 的问题。如果你有兴趣,推荐你去看《代码大全》的相关章节。

  1. No trackbacks yet.