Ruby 里的元编程

August 12, 2007 – 7:51 pm

关于元编程

Wikipedia 上关于元编程的定义说元编程就是将程序作为数据进行处理。“用程序来处理程序”,这就是“元”的来源了,这本身是一个容易产生混淆的地方,就像“用语言来描述语言”一样,数学上的许多悖论就来自于此呢。幸好我们用的编程语言比自然语言要简单许多,并且都有严格的定义规范,有兴趣的人可以尝试在自己喜欢的编程语言里面构造一下 “This statement is false” 这个经典悖论。

回到元编程,程序处理程序可以分为“处理其他程序”和“处理自己”,对于前者,有我们熟悉的 lex 和 lacc 作为例子。而对于后者,如果再细分,可以分为“宏扩展”、“源代码生成”以及“运行时动态修改”等几种。

宏扩展从最简单的 C 语言的宏到复杂的 Lisp 的宏系统,甚至 C++ 的“模板元编程”也可以包含在这一类里面,我在这里对它们进行了一些介绍。

源代码生成则主要是利用编程语言的 eval 功能,对生成出来的源代码(除了在 Lisp 这样的语言里面以外,通常是以字符串的方式)进行求值。有一类有趣的程序 quine ,它们运行的结果就是把自己的源代码原封不动地打印出来,通常要证明你精通某一门语言,为它写一个 quine 是绝佳的选择了,这里有我搜集的一些相关资料。

最后是运行时修改,像 Lisp 、Python 以及 Ruby 之类的语言允许在运行时直接修改程序自身的结构,而不用再经过“生成源代码”这一层。当然对源代码进行 eval 的方法也许是最灵活的,但是它的效率却不怎么样,所以相来说并不是特别流行。这里主要介绍的是这种方式的元编程在 Ruby 里面的应用,如果对元编程的其他方面感兴趣,前面的几个链接应该都是很好的去处。

类与对象

Ruby 是一门完全面向对象的语言,在 Ruby 里面,所有的东西都是对象。对象是什么?对象是类的一个实例。那么类又是什么?按照前面的说法,一切都是对象,那么类也应该是一个对象喽!没错,类也是一个对象。按照前面的说法,对象应该是某一个类的一个实例,这也没错,一个特定的类就是“类(Class)”这个类的一个实例。已经开始拗口了吧?因为已经开始接触到“元”了,也就是所谓的用自己来描述自己,而描述一个类的类,通常叫做“元类(metaclass)”,不过在 Ruby 里面,元类和普通类其实是同一个东西。事实上, Ruby 还是一个单根的面向对象语言,所有的类都继承自 Object 类,包括 Class 类。运行一下下面的单元测试代码就可以验证这一点(完整的代码可以在末尾找到下载链接)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
require 'test/unit'
 
class MetaTest < Test::Unit::TestCase
  def test_class
    str = String.new
 
    # str is an instance of the class String
    assert_equal String, str.class
    # the class String is an instance of the class Class
    assert_equal Class, str.class.class
    # the class Class is an instance of the class Class itself,
    # so metaclass is just normal class
    assert_equal Class, str.class.class.class
    # the class Class is also derived from the class Object
    assert_equal Object, str.class.class.superclass.superclass
  end
 
end

和其他面向对象语言一样。Ruby 里面对象保存成员变量的值,但是并不保存成员函数(方法),方法是放在对象所属的类里面。事实上,由于对象里面不会保存函数,Ruby 的底层实现里面类这种对象和普通对象还是有一定的区别的:

321
322
323
324
325
326
327
328
329
330
331
struct RObject {
    struct RBasic basic;
    struct st_table *iv_tbl;
};
 
struct RClass {
    struct RBasic basic;
    struct st_table *iv_tbl;
    struct st_table *m_tbl;
    VALUE super;
};

只是我们在 Ruby 里面看起来它们是一样的,或者说,类的类型派生自对象的类型,加了一个放函数的表和一个指向父类的域。

另外,还有一类方法叫做“类方法”,它不能引用到对象的成员变量,例如:

class Foo
  def Foo.func1
 
  end
 
  def Foo.func2
 
  end
end

而在类的环境里面 self 是指向类自己的引用,因此也可以用 self 代替代码中的 Foo ,像这样:

def self.func1

另外,Ruby 还允许另外一种写法:

class Foo
  class << self
    def func1
 
    end
 
    def func2
 
    end
  end
end

事实上,在类方法里面不能引用到对象的成员变量的原因很简单,因为类方法的 self 引用到特定的类,而成员方法引用到特定的对象。看下面的测试代码:

  def test_class_method
    # foo is an instance of class Foo
    foo = Foo.new
    # klass is the class of the object foo, i.e. Foo
    klass = foo.class
    assert_equal Foo, klass
 
    # class method of object foo is just instance method
    # of object klass(which is the class of foo)
    klass.set(10)
    assert_equal 10, klass.get
 
    # because get is not instance method of object foo
    assert_raises(NoMethodError) { foo.get }
  end

但是在 Foo 这个对象的类里面并不能找到 get 这个方法,如果说 getFoo 的成员方法的话,那么它应该包含在 Foo.class.instance_methods 里面才对。不过这里其实另有乾坤,关系到一个 singleton class ,我们将在后面再回到这个问题上来。

Ruby 里面的元编程

一个比较常见的例子当属 define_method 。Ruby 里面有 attr_accessor 方法,例如:

class Foo
  attr_accessor :foo, :bar
end

Foo 这个类就有了 foofoo=barbar= 这些方法,用于读取和设置 foobar 两个属性。使用起来很方便,其实一点也不神奇,用 define_method 我们立马就可以写一个,由于 Ruby 的类可以随时重新打开添加东西,我们直接修改 Class 类:

class Class
  def prop_accessor(*props)
    props.each do |prop|
      define_method prop do
        instance_variable_get "@#{prop}"
      end
      define_method "#{prop}=" do |val|
        instance_variable_set "@#{prop}", val
      end
    end
  end
end

在定义 Foo 的时候 self 是引用到 Foo (这是 Class 这个类的一个实例),因此可以使用 Class 类的成员方法,所以可以像前面的例子里面使用 attr_accessor 那样来使用这里定义的 prop_accessor ,效果是一样的。

对象真的不能持有自己的方法吗?

是的,对象不能持有自己的方法,在前面看到 Ruby 的实现代码里面 RObject 这个结构里面根本没有用于存放方法的表。但是我真的想让对象有它自己的方法,要怎么办呢?只有 RClass 结构能保存方法列表,于是就让 RObject 保存一个自己的 RClass 类就好了。这个类通常叫做“singleton class”,甚至也有叫“metaclass”的,但是通常我们认为“metaclass”是用于定义类的类,容易造成混淆,Matz 正在考虑在以后的版本里面把这个类叫做“eigenclass”,那么我们这里就称它为“eigenclass”好了。一个对象(例如 foo)的 eigenclass 与它所属的类(可以通过 foo.class 获得)有几点不同:

  • eigenclass 是对象所独有的,属于同一个类的不同对象拥有不同的 eigenclass 。
  • Ruby 目前没有提供像 foo.class 这样的方法来直接获得这个 eigenclass ,不过幸运的是我们可以很轻松地实现这个方法。
  • Ruby 会优先从 eigenclass 里面查找方法,其次才是对象所属的类以及各个祖先类。

虽然没有内置的轻松获取 eigenclass 的方法,但是将方法存放到 eigenclass 却是非常简单的,事实上语法我们已经很熟悉了。

  def test_singleton_method
    foo = Foo.new
 
    # now we put the singleton_func in the eigenclass of foo
    def foo.singleton_func
      self
    end
 
    # singleton_func exists in foo, and self refer to foo
    assert_equal foo, foo.singleton_func
    # singleton_func is not available in other instance of class Foo
    assert_raise(NoMethodError) { Foo.new.singleton_func }
  end

另外,还可以这样写

class << foo
  def singleton_func
    self
  end
end

是不是觉得似曾相识?没错,就是前面定义类方法的时候用的语法。可是那里点好前面是一个类,这里是一个对象,不是吗?没错!可是别忘了在 Ruby 里面,类也是一个对象!好了,让我们用 eigenclass 和 singleton_method 来解释一下前面的类方法的代码。

def Foo.func1

这是在 Foo 这个对象的 eigenclass 里面放入了 func1 这个方法。这也是我们在 Foo.class 里面找不到 func1 的原因了,因为它根本不在那里。现在我们已经迫不及待地想要取出 eigenclass 来看看了,怎么能容忍它偷偷地躲在那里呢?下面我们来给 Ruby 里面所有类的元祖 Object 类开开刀:

class Object
  def eigenclass
    class << self
      self
    end
  end
end

这么简单几行代码就 OK 了,在 eigenclass 方法内,打开 self(这里引用到当前的对象) 的 eigenclass ,打开以后里面那个 self 就引用到我们要的那个 eigenclass 了,注意最后一行是作为返回值的,这样我们就成功取出了 eigenclass ,赶紧测试一下!

  def test_eigenclass
    foo = Foo.new
 
    def foo.singleton_func
      self
    end
 
    # eigenclass is also an instance of the class Class
    assert_equal Class, foo.eigenclass.class
    # singleton_func is put as an instance method in the eigenclass
    assert foo.eigenclass.instance_methods.include?('singleton_func')
    # so here is the magic of the so called class method
    assert Foo.eigenclass.instance_methods.include?('get')
  end

回到类方法的问题上。我们进行如下定义以方便描述

foo = Foo.new
klass = foo.class
metaclass = klass.class
eigenclass = klass.eigenclass
  • foo 的成员方法很简单,它是存放在 klass 中的方法。也就是 klass 作为一个类所拥有的方法,调用 klassinstance_methods 就可以得到这些方法。
  • foo 的类方法,它是 klass 的成员方法,但是并不是像普通的成员方法那样存放在 metaclass 中,而是存放于 klass 的 singleton class eigenclass 中。

define_singleton_method

singleton method 并不是 Ruby 独有的,在 CLOSDylan 等语言里面也有,而像 SelfNewtonScript 之类的基于原型的语言甚至只有 singleton method 。Ruby 里面采用这种方式漂亮地实现了“类方法”。遗憾的是,目前的 Ruby 里面并没有提供一个和 define_method 同样功能的用于定义类方法的方法,我们只好自己写一个了。

class Object
  def define_singleton_method name, &body
    eigenclass.send :define_method, name, &body
  end
end

由于 define_method 是私有方法,因此我们需要使用 send 来调用。或者也可以使用 instance_eval 的方式来实现。另外,在类里面使用的时候,我们可以用一个更明确的名字:

class Class
  alias define_class_method define_singleton_method
end

Dwemthy’s Array 是一个很有趣的 Ruby 元编程的例子。仅用了几十行代码就写好了一个 RPG 游戏。在这里我们来把它的 Creature 类的 traits 类方法简化一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Creature
  def self.traits(*arr)
    return @traits if arr.empty?
 
    attr_accessor *arr
 
    arr.each do |trait|
      define_class_method trait do |val|
        @traits ||= { }
        @traits[trait] = val
      end
      define_method :initialize do
        self.class.traits.each do |k, v|
          instance_variable_set("@#{k}", v)
        end
      end
    end
  end
end

参考文献

附件

Testcase

  1. 7 Responses to “Ruby 里的元编程”

  2. zz到俱乐部blog上吧~~
    下学期靠你开讲座啦

    By zhouyuan on Aug 13, 2007

  3. “copy and paste” is the evil…

    By Jack on Aug 13, 2007

  4. 好像没有再说meta programming….

    By Jack on Aug 13, 2007

  5. to jack:
    哦?难道这些不是元编程的东西吗?

    By pluskid on Aug 14, 2007

  6. to zhouyuan:
    在学校讲 Ruby 这些东西没人听呢!哈哈!

    By pluskid on Aug 14, 2007

  7. to zhouyuan:
    在学校讲 Ruby 这些东西没人听呢!哈哈!

    ———————————

    关键是宣传、包装策略

    By zhouyuan on Aug 14, 2007

  8. 看了看你的博客,学到了很多东西
    非常感谢,呵呵

    By luonet on Aug 20, 2007

Post a Comment