• 2007-10-31

    一段Ruby代码的解释

    Tag:Ruby
    阅读Rails源码的时候,会发现代码中遍布着一些看上去比较奇怪的代码,大概会是这个样子:
      people.collect(&:name)

    这段代码实际上等价于
      people.collect { |p| p.name }

    但是,从Ruby的语法上来看,这行代码看上去是很难理解的,主要就是&:name。把这行代码当作Ruby脚本直接运行,你会发现,Ruby会向你报错,错误是:
      wrong argument type Symbol (expected Proc) (TypeError)

    那么为什么这样的代码遍布于Rails,却能正常运行呢?

    最初困扰人的可能主要是&:name的写法,这看上去根本不像正常的Ruby语法,但只要将它进行适当的分解,我们就豁然开朗了。在Ruby中,“&”通常用来表示后面跟的是Proc,而:name在Ruby中表示一个Symbol。把二者结合在一起,矛盾变产生了,”&”要的是一个Proc,而我们给的一个Symbol,这就是前面错误的来源。

    知道了错误的来源,接下来的问题是,Rails里究竟变了怎样的魔术,让这段代码通过呢?

    其实,“&”后面如果跟的不是一个Proc,那么它会试图找寻一个Proc,在Ruby中,这意味着它会调用to_proc方法。这是Ruby中的一种标准协议,关于这种转换协议,可以参考《Programming Ruby》中《Duck Typing》一章相关的介绍。

    由于后面的是一个Symbol,结合前面的说法,只要为Symbol类提供一个to_proc方法,至少在语言层面上,就会变得正确起来。事实上,Rails正是这样做的。

    class Symbol
      def to_proc
        Proc.new { |*args| args.shift.__send__(self, *args) }
      end
    end
    (activesupport/lib/active_support/core_ext/symbol.rb)

    如果说to_proc仅仅是让这个魔术在语言层面上通过,那么上面这段代码也解开了其余部分的神秘面纱。不妨再进一步,看看它究竟是如何做到的。

    通过开始的代码等价对比,我们已经知道了这行的代码意义,主要也就是调用了一个对象的方法。将它对应到Proc.new所附带的block上,我们便不难看出这段代码的意义所在了。

    这里的*args是block的参数,在前面的例子中,p就是这个参数,我们要调用p的name方法,所以,p是作为receiver的,而args.shift正是将p提取了出来。通过__send__,我们就可以调用receiver的方法,而__send__需要一个Symbol指定要调用的方法,别忘了,我们正是在一个Symbol类中定义方法,于是self成为了一个自然的选择。至于剩余的参数(对*args调用shift之后),就作为参数传给方法了。实际上,大多数用法中,只有一个参数,所以,剩余的部分会是一个空数组。

    通过这种变换,p.name就等价于args.shift.__send__(self, *args)了。

    关于这段代码的实现,也许Ruby Extensions的实现更加清楚一些。
      def to_proc
        proc {|obj, *args| obj.send(self, *args) }
      end
    这段代码将receiver分拆出来,所以,更加容易理解。当然,严格的说,二者还是稍有差别,这种实现必须要有一个参数。但是,在大多数情况下,这种实现已经足够了。
  • 作为一个程序员,获取知识是让我不断前进的动力,而读书是我获取知识的一条重要途径。在这个“经典”、“必读”过剩的年代里,大多数的书都仅仅扮演着传播知识的角色,真正改变自己对某些问题看法的书其实少之有少。限于读书时的眼界和能力,在我列表中,让我拍案惊奇的书只有几本。Martin Fowler的《重构》,严格说来,我并没有完整的读完这本书,不过,正如作者自己所说,这样的书原本就不指望能够读完,因为有一大部分其实是参考手册。正是我读过的部分让我知道了重构,让我知道这么做可以把代码写得更好。Robert Martin的《敏捷软件开发》,这是一本名字赶潮流,内容很丰富的书,这本书让我开始理解软件设计,从此不再刻意追求设计模式。Kent Beck的《测试驱动开发》,我读的是英文版,因为当时中文版还没有出版,所以,我不敢说,我通过这本书很好的理解了测试驱动开发,但它却为我打开了一扇门,让我知道了一种更好的工作方式。

    有好长一段时间,这个列表没再更新过,中间虽然我也读了很多书,也学到了很多东西,但却没有哪本书如这几本书一样给我带来巨大触动。新近加入我这个列表的书是《修改代码的艺术》,英文名是《Working Effectively with Legacy Code》。

    对于很多软件开发人员来说,加入一个公司,通常意味要面对一大堆之前留下的代码。而面对沉重的负担,大多数人的感觉都是无可奈何。让无奈成为往事,也就是这本书的价值所在。

    在我看来,这是一本讲解如何编写测试的书。之所以遗留代码让人头痛,除了复杂的逻辑,改动会带来怎样的后果是一件让人心里没底的事,而测试的存在可以大幅度降低这种恐惧。但是,许多代码在开发时并不考虑测试,这样做的结果就是让测试几乎成为一件不可能完成的任务,一个常见的例子就是代码中访问数据库。即便写出测试代码,漫长的测试过程也会让它失去一部分应有的作用,我们希望得到的是快速的反馈。所以,对于
    无测试而言,知道编写测试是一种境界的提升,写好单元测试则是一种更高的境界。如果能够让测试驱动开发,从开发之初便考虑测试,并懂得如何写好测试,开发者应该不会陷自己于一种难为的境地,这也应该成为专业程序员应该具备的基本技能。

    至于这本书的具体内容,我的评价是实用。具体的手法,很难在这里一一列举,但是,以我的开发经验来看,
    许多似曾相识的代码不断的出现在书中,而作者举重若轻的处理手法,正是让我有拍案惊奇的地方。实际上,回味起来,每个手法都不是什么很高超的技法,但正是因为见识过类似的代码,才能体会到这种手法的价值所在。所以,相对于程序新人,它更适合有经验的人。

    之所以说这本书更适合有经验的人还因为,这本书中谈及的内容涵盖设计、测试、重构等诸多方面:通过重构,解开代码内的耦合,让其可测。这恰恰是前面提到的那三本书所讲的内容。也只有懂得了这些基本内容才能体会到那些具体手法的价值所在。依然记得当年读《重构》时,在提取和内联之间迷茫了好久,直到后来经过了许多开发实践才体会到这些做法的真正含义。

    如果说不足,那么,这本书缺乏一个列表,就像Martin Fowler为《重构》所做的那样,出什么样的问题,应该采用怎样的手法进行处理。

    关于中译本,总的来说,翻译得很流畅,读起来比较舒服。不过,制作上还是有一些不太让人满意的地方。
    * 译注太多,而且有些是低估读者智商的译注。
    * 页边标有页码,似乎是为了与英文版对照,但文中的参考页码又是以中文版为准,显得有些乱。
    * 书的装订不是特别令人满意,我一直担心从中间断开。
  • XRuby:享用JVM上的Ruby

    在InfoQ China发了一篇介绍XRuby的文章。其实,对于之前听过我介绍XRuby的人来说,这篇文章的内容并不新鲜,因为基本上,这篇文章的内容脱胎于之前介绍XRuby的讲稿。虽然讲了几次,但还是应该把这篇文章写出来。一来,到场听介绍的人毕竟是少数,写出来看到的人应该可以更多,也让更多的人有机会了解XRuby,再有,内容写成文章需要比演讲时有更多的思考。所以,整体来说,内容叙述应该会更加准确。

    这是一篇早就该写的文章,至少最初答应霍泰稳写这篇文章还是5月份的时候,7月份录我InfoQ访问的时候,又答应了Floyd完成这篇文章,可真正发布已经是十月份了。不过,这样一拖再拖也不是完全价值。在这段时间里,我在Agile Day讲了一次Ruby on JVM,让我对这个方面有了些新的思考,特别是把Ruby放在 JVM上的价值,这一点已经体现在这篇文章里了。另外,XRuby自身在这段时间中也发生了很大的变化,特别是Annotation的加入,让代码在表现形式上得到很大的进步。至少在我看来,最终体现在文章中的示例代码是可以接受的。

    我希望,这篇文章可以成为一个起点。一方面,它可以作为让更多人了解XRuby的起点;另一方面,XRuby团队把它作为一个起点,向其它人展示XRuby中非常优秀的一面。当然,XRuby现在已经有了不少不错的文档。

    已经有朋友给我建议,写一些更深入的东西,这也是我所希望的,只探讨一些比较浅的东西不过瘾。在XRuby开发过程中,有很多有趣的思考,我很愿意与人分享那种开发中的快乐。再有,写东西会促使人思考,随之而来的往往是发现不足,这也是有益于XRuby进一步改进的。

    如果你希望了解或参与XRuby,不妨告诉我们,你想了解什么,也许,我们之后的文章会满足你!
  • 2007-10-10

    编译与解释

    Tag:向下走
    如果我们想将程序设计语言编写的源代码运行起来,通常情况下,我们有两条路可以走:解释和编译。

    计算机能够认识的只是01串,所以,我们编写的源代码要想真正启到应有的作用,必须经过转换,转换成一种可执行的格式,然后,由专门的执行引擎将它运行。

    解释,通常是将源代码解析为一个中间形式(比如抽象语法树,AST),然后,经由一个软件的执行引擎执行这个中间形式,产生对应的结果。这种做法的好处就是可以把执行逻辑独立处理,无须为每个平台编写不同的代码,所以,这种做法的可移植性很好,于是,它成了不少程序设计语言最初的选择。不过,随之而来的问题就是这会降低执行速度,毕竟,这个软件执行引擎的性能无法与硬件相比。所以,以硬件作为执行引擎的“编译”会让程序拥有更佳的性能。

    采用编译的方式,我们可以将代码编译成可以由硬件直接执行的二进制代码。因为不同硬件和不同操作系统的二进制格式是不同的,所以,如果希望语言得到广泛应用,那便需要针对不同平台实现平台相关的编译器后端,理论上说,这不是一件不能完成的任务,但这意味着巨大的工作量。虽然编译语言的数量不在少数,但事实上,真正能够跨越各种软硬平台的编译语言似乎只有C。

    虚拟机的出现让人们找到了在软硬件执行引擎之间的一个折衷。一方面,编译器只要生成针对虚拟机的代码,而不必为各种各样的软硬件平台费心,另一方面,虚拟机不断的优化可以让程序在不受编译器的影响下越跑越快。所以,许多程序设计语言走上了这条路,比如Java,比如Python。这些语言大多采用的是自行设计的虚拟机,但这个做法虽然可以充分的将语言特性与虚拟机结合起来,但无疑也意味着大量的重复工作。Parrot希望成为一个集大成者,为众多动态语言搭建一个共同的平台,只是不知何年何月才能实现它宏大的目标。

    Java虚拟机(JVM)的普及为这个问题提供了另外一种选择。因为主流软硬件平台上基本都有自己的JVM实现,所以,只要生成针对JVM的代码——字节码,便意味着可以运行在大多数平台上。当然,虚拟机也是一种软件实现,所以,性能上也会有一些损失。不过,从Java平台的广泛应用也证明了,这样的损失在实践中是可以接受的。随着JVM技术上的不断进步,性能损失越来越小。再者,作为一种为静态语言设计的平台,目前,JVM自身并不支持动态语言特性,所以,要想让动态语言语言运行在上面,需要额外做一些工作,搭建一套支撑动态语言的结构。不过,随着一些动态语言逐渐被移植到JVM上,人们也意识到了这个问题上,开始考虑在JVM中增加动态语言的支持。

    除了执行性能之外,编译带来的好处还在于保护源代码。因为解释方式通常不会保留的其中间形式,所以,产品发布意味着将源代码也发布出去。对于需要保护知识产权的公司和个人而言,这是他们所不愿意看到的。通过编译的方式,我们将源代码转成了二进制,这样,源代码可以得到有效保护。当然,破解二进制也是有可能的,不过,那就是另外的故事了。