• 此刻,我,在上海。

    距离上次离开上海还不到两个月,之所以重返大上海,是为了RubyConf China。

    刚听说RubyConf China要举办的消息时,我就在想,找个怎样的借口才能到上海参会。所以,当我们负责市场同事找到我,说需要一个演讲人的时侯,我毫不犹豫的就接受了任务。

    接受任务是喜悦的,完成任务却是烦恼的。先是gigix和WPC讨论演讲的主题,最终定下分享我们在Ruby项目的一些经验,接下来,就是具体的准备了。每次准备演讲对我来说,都是一个痛苦的过程:一方面,我希望自己的演讲对听众来说,有一些价值,另外一方面,我还要保证思路顺畅,不让人感到突兀。这一次的准备,尤其困难,因为得到确切消息距离大会只有一个星期了,而且我还在项目上。感谢gigix和WPC等人,在他们的帮助之下,我对要讲的内容逐渐有了一个更加清晰的认识,也是因为如此,演讲稿也迟迟达不到可以拿出手的标准。负责联系演讲者的Daniel几次催我提交演讲稿,我何尝不想啊!演讲前夜,我依然在不断修改。

    这次RubyConf China最大的主角,自然是Ruby之父Matz先生。从他进场时那经久不息的掌声中,就可以看得出他到底有多受欢迎。他的演讲《Why Ruby?》介绍了他开发Ruby的一些历史,以及他在设计程序设计语言的一些考虑。很多想法也与我的一些观点不谋而合,听得我频频点头。Ruby原来是一个失业者的产品,时值日本经济不景气,失业在家的Matz利用这段难得的空闲,写出了Ruby。另一个失业者著名产品BT。很多人内心难以接受的失业原来也是一个巨大的资源。Matz说,如今这样一个不景气的光景,说不准就会产生出下一个影响深远的语言。

    借助演讲者的身份,我让会议的组织者引荐我认识了Matz。时间关系,我只和Matz做了一个非常短的交流,介绍了自己在Ruby实现上的一些工作,算是和Matz彼此相识了。最后,我邀请Matz到我们的北京办公室去做客,Matz欣然应允,“只要你们邀请我就好”。

    这次大会的内容还很扎实,robbin的《JavaEye网站架构深度解密》和robinlu的《Ruby and Rails Pitfall》是两个亮点,拳拳到肉。只有真正的经历过这样的问题,才能讲得出这样的话。从演讲技巧上来说,他们两个都算不上优秀,但扎实的内容让人已然忘却了外在,全心全意追随他们的内在。

    koz的《岛根县政府的挑战 - 在日本地区政府和社区当中使用Ruby的案例》是一个介绍,让我们了解Ruby在日本的发展,很开眼。让我惊讶的是,他——一个日本人用中文完成了整个演讲。Ruby的流行程度超乎了我的想像,可以说Ruby教育,从娃娃抓起。原来,岛根县就是Matz居住的地方,岛根县对于Ruby的推动,让我想起了鸟山明,政府为了让他送稿方便,而修了一条高速公路。

    其它几个演讲带给我的冲击相对来说要小得多。至于我自己的演讲,留给别人评价吧!

    最后的Q&A环节,说实话,我更想坐在下面做一个可以提问的听众,而不是一个回答问题的演讲者,因为对于其他人的演讲,我还是希望有更多的交流。在这个环节中,有一个人用日英中三种语言提问,成了大家关注的焦点。

    整体来说,这次RubyConf China还是很成功的,考虑到组织者只用了一个月时间,这样的结果已经让人非常满意了。robbin说,争取下半年在北京在办一次。北京的Ruby开发者们,期待吧!

  • 作为Ruby程序员的你正在编写一个权限管理的模块,受到DSL风潮的影响,你希望用户这样使用它:
    class User
      include Permission

      grant :operation, Role::Admin
    end

    这样的话,意味着grant是一个类方法,于是,按照惯性思路,你写出了这样的代码:
    module Permission
      def self.grant operation, *roles
        ..
      end
    end

    兴奋的你迫不及待的开始运行这段代码,不过,Ruby解释器却无情的告诉你:
      undefined method `grant' for User:Class (NoMethodError)

    这么会这样?我明明是定义了grant方法!

    你知道,写代码的时候,烦躁不会给你带来任何好处,冷静思考才是最好的出路。所有关于模块的一切开始在你脑海中浮现,以往,我们在类中包含模块的方法都是实例方法,包含类方法还真的是头一遭,难道Ruby只能包含实例方法?源码是最好的答案,于是,你决定翻出Ruby的实现一探究竟(《管窥Ruby——类层次结构》)。

    在构造include类的时候,你看到了这样的代码:
    static VALUE
    include_class_new(module, super)
        VALUE module, super;
    {
      ...
      klass->iv_tbl = RCLASS(module)->iv_tbl;
      klass->m_tbl = RCLASS(module)->m_tbl;
      klass->super = super;
      ...
    }

    这里明白无误的告诉你,在构造include类时,只将m_tbl(方法表)赋值过去,也就是说,只有实例方法会随着模块一起包含到类里面,只有模块级别上的方法,那只能说声抱歉了。

    难道这是一个不能跨越的天堑?你显然并不甘心就此打住。你突然想起,有一些Rails的plugin确实做到了这一点,于是你翻出了一些Plugin的实现,你发现了一些蛛丝马迹,照猫画虎,你也写出了类似的代码:
    module Permission
      def self.included(base)
        base.extend PermissionClassMethod
      end

      module PermissionClassMethod
        def grant operation, *roles
          ..
        end
      end
    end

    这一次,你小心翼翼的把这段代码送给了Ruby解释器,它居然通过了!

    仅仅拥有可运行的代码是不够的,你需要一个合理的解释,否则,这段代码会是你心中一直的不安。你开始尝试理解这段代码的运作机制。

    self.included,你记得你阅读Ruby实现的时候,曾经有缘相识,它实际上是一个回调方法,当一个模块被其他类(或模块)包含的时候,这个方法就会得到调用,而传入的参数,就是包含这个模块的类(或模块)对象。剩下的部分就是一马平川了。对这个对象进行扩展(extend),扩展模块中的方法,就会变成这个对象自身的方法。因为这里的对象是类对象,类对象的方法自然就变成了类的方法。到这里,你一下子豁然开朗。

    你发现,原来这里并不像看起来那么神奇。基于你对Ruby实现的了解,你发现除了运行included这个回调方法之外,用append_features同样可以解决问题,因为append_features也是一个会在包含模块过程中调用的方法。

    突破了这里,你的心平稳下来。于是,你回到原来的路上,继续你DSL体验之旅!
  • 曾经,我听到了太多关于Rails的评价,不过,只有经历了,才会了解真相,很高兴自己在一个真正的Rails上项目上摸爬滚打了几个月。之前大家大多没有Rails的经验,于是,我们体验到了原生态的Rails开发。

    在我看来,MVC的核心在于M,ActiveRecord是一个很好的起点。

    ActiveRecord用起来非常简单,比如修改一个字段,我们可以这样做:
      def spotlight!
        record.spotlight = true
        record.save!
      end
    正是因为这样做极其简单,所以,对于一组记录而言,就很容易写出这样的代码:
      records.each(&:spotlight!)
    我们知道,上面的保存操作,最终会对应成一条SQL语句,所以,下面的集合操作就会生成很多条SQL语句。只要一提醒,相信大多数都会发现,对那一组记录的操作,一条SQL就可以搞定。

    随着项目的进展,Model文件逐渐增大,里面的代码越来越多,显然,大类是不受欢迎的。我们都知道,Ruby是一种动态语言,一个重要的优势就是它的类可以重新打开。换句话说,对于这种越来越大的类而言,我们是可以把其中的方法按照逻辑进行分离,将其置于不同的文件里面。前不久,和一个做Java的同事在探讨Domain Model如何组织,他也遇到的类似的问题,不过,显然,Java在这方面没有Ruby这样生就的能力。

    用划分文件的方式,显然可以在一定程度上解决Model逐渐增大的文件。不过,对于这个问题,还需要从另外一个角度思考,为什么Model会越来越大。在Java开发中,我们会很明确的划分出Model层和Service层,在Controller中,我们会调用Service层来解决问题。而在Rails开发中,我们通常会直接使用Model,实际上,这等价于把Java开发中的Service和Model的逻辑都放在了Model中,这也是让Model逐渐增大的一个原因。所以,这就给了我们一个机会,Model里面的东西,真的都属于Model吗?也许,在Rails开发中,我们不习惯像Java开发那样严格划分出Service层,但是这并不是说,就应该把所有的东西都放到Model中。所以,当我们一次又一次面对那个越来越庞大的Model时,也许,我们需要做的是做一番设重构,让那部分不该属于Model的代码,回归到它应该在的位置。

    说Model一力承担了Model和Service两个角色应该承担的工作,是一种谬奖。除了Model之外,Controller往往会在功劳簿有浓重的一笔。正如我最开始提到ActiveRecord实在够简单,它的字段都是可以直接在外面访问的,所以,看到在Controller里面直接调用也不应该感到意外。有了开头,后面就会变得顺理成章,于是,一个厚重的Action就此诞生了,同样,我们也由此得到了一个练习重构的好机会。

    Rails功高盖主,于是就出现了Rails程序员不一定对Ruby了解很多的现象。结果就是,Ruby语言的特性并不能得到很充分的应用。比如说,有些helper方法会在不同的页面上起作用,对Rails程序员而言,最直接的反应是在Controller里面声明一个helper。也许把这些方法放在一个Module里面,然后让这些Helper包含这个Module似乎更加符合Ruby程序员的胃口。同样,Ruby有很强的元编程的能力,这个能力显然有助于写出更加简短的代码,但Rails程序员却不见得会充分利用这些特征。

    准确的说,我上面列举出的这些问题,并不是Rails本身的问题,比如前面提到的Model和Controller方面,更多的是一个如何设计的问题,只不过,用Rails这种轻快的工具,杀得兴起之际,往往会忽略这些应该注意的问题。挖下的坑,早晚要人来填,为了他们(也许就是自己)的健康,运用这些工具时,再理性一点!

  • 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分拆出来,所以,更加容易理解。当然,严格的说,二者还是稍有差别,这种实现必须要有一个参数。但是,在大多数情况下,这种实现已经足够了。
  • 今天是一个发布的日子,XRuby发布了0.3.1,Ruby Hacking Guide中文版发布了第一部分。

    XRuby 0.3.1

    相比于前一个版本,XRuby 0.3.1最大的进步在于完成标准库的预编译。预编译意味着什么?标准库代码无需在每次运行时编译,这意味着今后使用XRuby的标准库性能会得到一定的提升。

    有一个与编译相关的话题。之前,Jon Tirsen曾经谈到JRuby的一个问题,运行在AppServer中会有占用太多内存。经过分析得知,为了提高程序的并发性,程序运行会启动多个JRuby。每个JRuby解析Ruby脚本都会建立一棵完整的语法树,这就意味着,由于这种解析模式本身的限制,对于同样的内容,内存中需要保存多份相同的语法树,这种做法意味着无谓的耗用了大量的内存。采用编译的做法,则可以很好的避免这个问题。因为在运行时,相同的是字节码,而JVM很好的帮我们解决字节码共享问题,无需耗用大量的内存。

    Ruby Hacking Guide中文版第一部分

    RHG终于完成了第一次发布。已经发布的第一部分介绍的是Ruby的对象模型。我正是从这个部分开始了解Ruby实现的,进而完成了XRuby的Runtime的重写。所以,我一直觉得这部分是了解Ruby实现非常好的一个起点。

    从翻译Ruby Hacking Guide到现在已经超过了一年,从第一次发布消息算起也超过了9个月。相比XRuby,这个项目的进展可以用异常缓慢形容。这是一本日文书,也是一本技术书,而且是一本讲语言实现的书。任何一个点都会增加翻译的难度。几个懂日语的朋友先进行一遍初译,然后,我对再对译稿进行一遍校验,并根据自己的理解修改译稿,这样的过程无疑延长了处理的时间。这是一个业余时间的项目,而我更多的业余时间在XRuby上,没有太多精力投入上面。种种的因素造成了这个项目的一托再托。

    目前,我手头已经有了第二部分全部和第三部分几章的初译稿,不过,按照之前的进度来看,这几章的发布可能要等到许久之后了。如果你有兴趣,可以加入到这个项目中来,这样,有助于加快这个项目的进度。