Metaprogramming Ruby. The Core. 《Ruby元编程》读书笔记

WARNING: The following is the study notes of "MetaProgramming Ruby". As the only academic research, if you like the content, please support genuine.

以下内容为《Ruby元编程》的读书笔记,仅作为学术研究,如果您喜欢以下内容,请支持正版.

I. The object model

Open Class

1
2
3
4
5
class String
  def to_alphanumeric
    gsub /[^\w\s]/,''
  end
end

Inside Class Definitions

1
2
3
4
5
3.times do
  class C
    puts 'hello'
  end
end

class关键字把你带到类的上下文,让你可以在其中定义方法。

What's in an Object

1
2
3
4
5
6
7
8
class MyClass
  def my_method
    @v = 1
  end
end

obj = MyClass.new
obj.class # => MyClass

实例变量

1
2
obj.my_method
obj.instance_variabes  #=> [:@v]

方法

1
obj.methods.grep(/my/) # => [:my_method]

一个对象仅包含它的实例变量和一个对自身类的引用。

方法存放在类中,而非对象中。

1
2
String.instance_methods == "abc".methods  # => true
String.methods == "abc".methods  # => false

Classes Revisited

类自身也是对象。类也有自己的类,名为Class。类的方法就是Class的实例方法。

1
2
"abc".class # => String
String.class # => Class
1
2
3
String.superclass # => Object
Object.superclass # => BasicObject
BasicObject.superclass # => nil
1
2
Class.superclass # => Module
Module.superclass # => Object
1
2
class MyClass; end
obj1 = MyClass.new

obj1 和 MyClass 都是引用,唯一区别在于,obj1是一个变量,而MyClass是一个常量。

Alt text

Constants

任何以大写字母开头的引用(包括类名和模块名),都是常量。

常量像文件系统一样组织成树形结构。模块(类)像目录,而常量像文件。

常量可以通过路径方式来唯一标示。使用双冒号进行分割。

1
2
3
4
5
6
7
module M
  class C
    X = 'a constant'
  end

  C::X
end

在常量前加上一组冒号标示根路径

1
2
3
4
5
module M
  Y = 'another constant'
end

::M::Y

Method Lookup

接受者(receiver):你调用方法所在的对象, 例如mystring.reverse()中,mystring就是接收者。

祖先链(ancestors chain):一个类移动到它的超类,再移动到超类的超类,依此类推,直到到达BasicObject类。

1
2
3
4
5
6
7
8
9
10
11
12
13
module M
  def my_method
    'M#my_method()'
  end
end

class C
  include M
end

class D < C; end

D.new.my_method() # => 'M#my_method()'

include一个模块时,Ruby创建了一个封装该模块的匿名类,并把这个匿名类插入到祖先链中,在它的类上方。

D.ancestors #=> [D, C, M, Object, Kernel, BasicObject]

封装(wrapper)类叫做包含类(include class),也叫代理类(proxy class)

Method Execution

每一行代码都会在一个对象中被执行——这个对象就是当前对象。当前对象可以用self表示。

调用一个方法时,接收者就成为self。

在类和模块定义中(并在任何方法定义之外),self为当前模块。

1
2
3
class MyClass
 self # => MyClass
end

实例变量永远都被认定为self的实例变量。

没有明确指定接收者的方法调用,都当成是调用self的方法。

II. Methods

Calling methods Dynamically

1
2
3
4
5
6
7
8
class MyClass
  def my_method(my_arg)
    my_arg *2
  end
end

obj = MyClass.new
obj.my_method(3) # => 6
1
obj.send(:my_method, 3) # => 6

通过send()方法,调用的方法名可以成为一个参数。

在代码运行期间,最后一刻才决定调用哪个方法,叫动态派发(Dynamic Dispatch)

send()可以调用任何方法,甚至私有方法。在不使用上下文探针(Context Probe)情况下,这是查看私有事物最简单的方法。 Ruby 1.9.1新增了public_send()方法,尊重接收者的隐私权。

Defining Methods Dynamically

可以用Module#define_method()方法定义一个方法。

1
2
3
4
5
6
7
8
class MyClass
  define_method :my_method do |my_arg|
    my_arg * 3
  end
end

obj = MyClass.new
obj.my_method(2) # => 6

在运行时定义方法的技术称为动态方法(Dynamic Method)

method_missing()

1
2
3
4
5
6
class Lawyer; end

nick = Lawyer.new
nick.talk_simple

=> NoMethodError: undefined method 'talk_simple' for #<Lawyer:0x3c848> [...]

Kernel#method_missing()方法抛出一个NoMethodError。

Ghost Methods

method_missing()方法处理的消息,从调用者角度看,跟普通方法没有区别,但是实际上接收者并没有相对应的方法。这被称为一个幽灵方法(Ghost Method)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyOpenStruct
  def initialize
    @attributes = {}
  end

  def method_missing(name, *args)
    attribute = name.to_s
    if attribute =~ /=$/
      @attributes[attribute.chop] = args[0]
    else
      @attributes[attribute]
    end
  end
end

icecream = MyOpenStruct.new
icecream.flavor = 'vanilla'
icecream.flavor # => "vanilla"

Dynamic Proxies

一个捕获幽灵方法调用并把它们转发给另外一个对象的对象(有时也会在转发前后包装一些自己的逻辑),称为动态代理(Dynamic Proxy)

使用Ruby的delegate库快速实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
require 'delegate'

class Assistant
  def initialize(name)
    @name = name
  end

  def read_email
    "#{@name} read email."
  end

  def check_schedule
    "#{@name} check schedule."
  end
end

class Manager < DelegateClass(Assistant)
  def initiailize(assistant)
    super(assistant)
  end

  def attend_meeting
    "attend meeting."
  end
end

frank = Assistant.new("Frank")
anne = Manager.new(frank)
anne.attend_meeting # => "attend meeting."
anne.read_email # => "Frank read email."
anne.check_schedule # => "Frank check schedule."

DelegateClass()是一种拟态方法(Mimic Method),这种方法创建并返回一个新的Class。这个类会定义一个method_missing()方法,并把对它发生的调用转发到被封装的对象上。

When Methods Clash

当一个幽灵方法和一个真实方法发生名字冲突时,后者胜出。

为安全起见,应该在代理类中删除绝大多数继承来的方法,这就是白板(Blank Slate)类。 有两种途径删除一个方法。使用Module#undef_method()方法,它会删除所有的(包括继承来的)方法;使用Module#remove_method()方法,它只会删除接收者自己的方法,而保留继承来的方法。

Ruby 1.9 开始,白板技术被集成到语言自身中。 从BasicObject继承来的类会自动称为白板类。

1
2
BasicObject.instance_methods
 => [:==, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__, :__id__]

Wrapping It Up

使用动态方法动态派发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Computer
  def initialize(computer_id, data_source)
    @id = computer_id
    @data_source = data_source
    data_source.methods.grep(/^get_(.*)_info/) { Computer.define_component $}
  end

  def self.define_component(name)
    define_method(name){
      info = @data_source.send "get_#{name}_info", @id
      price = @data_source.send "get_#{name}_price", @id
      result = "#{name.capitalize}: #{info} ($#{price})"
      return " * #{result}" if price >= 100
      result
    }
  end
end

使用动态代理白板技术。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Computer
  instance_methods.each do |m|
    undef_method m unless m.to_s =~ /^_|method_missing|respond_to?/
  end

  def initialize(computer_id, data_source)
    @id = computer_id
    @data_source = data_source
  end

  def method_missing(name, *args)
    super if !respond_to?(name)
    info = @data_source.send("get_#{name}_info", args[0])
    price = @data_source.send("get_#{name}_price", args[0])
    result = "#{name.to_s.capitalize}: #{info} ($#{price})"
    return " * #{result}" if price >= 100
    result
  end

  def respond_to?(method)
    @data_source.respond_to?("get_#{method}_info") || super
  end
end

III. Blocks

Back to the Basics

1
2
3
4
5
def a_method(a,b)
  a + yield(a,b)
end

a_method(1,2) {|x, y| (x+y)*3 } # => 10

大多数程序员会对只有一行的块使用大括号,而对多行的块使用do..end关键字。

只有在调用一个方法时才可以定义一个块。块会被直接传递给这个方法,然后该方法可以用yield关键字回调这个块。块中最后一行代码执行的结果会被作为返回值。

通过Kernel#block_given?询问当前的方法调用是否包含块。

1
2
3
4
5
6
7
def a_method
  return yield if block_given?
  'no block'
end

a_method # => "no block"
a_method { 'here's a block!' } # => "here's a block!"  

Closures

当代码运行时,它需要一个执行环境: 局部变量、实例变量、self……既然这些都系都是绑定在对象上的名字,就可以简称为绑定(binding)

可以运行的代码由两部分组成:代码本身和一组绑定。

1
2
3
4
5
6
7
def my_method
  x = "goodbye"
  yield("cruel")
end

x = "hello"
my_method { |y| "#{x}, #{y} word"} # => "hello, cruel world"

当创建块时会获取到局部绑定(比如上面的x),然后把块连同它自己的绑定传递给一个方法。

上面的例子,块的绑定包括一个名为x的变量。虽然在方法中定义了一个变量x,块看到的x也是在块定义时绑定的x,但时方法中的x对这个块来说是不可见的。基于这种特性,块被称为闭包(closure)

Scope

作用域是所有绑定寄居的场所。

程序会在三个地方关闭前一个作用域,同时打开一个新的作用域: 类定义、模块定义、方法。 这三个边界分别用classmoduledef关键字作为标志。每一个关键字都充当一个作用域门(Scope Gate)

1
2
3
4
5
6
7
8
9
10
11
12
v1 = 1
class MyClass             # 作用域门: 进入class
  v2 = 2
  local_variables         # => ["v2"]

  def my_method           # 作用域门: 进入def
    v3 = 3
    local_variables
  end                     # 作用域门: 离开def

  local_variables         # => ["v2"]
end                       # => 作用域门: 离开class

在类和模块定义中的代码会立即执行。相反, 方法定义中的代码只有在方法被调用时执行。

Flattening the Scope

怎样让绑定穿越一个作用域门?

1
2
3
4
5
6
7
8
my_var = "success"
class MyClass
  puts my_var

  def my_method
    puts my_var
  end
end

上面的代码会报错。在进入另一个作用域时,局部变量会立即失效。如果能用方法代替class,就能在一个闭包中获得my_var的值,并把这个闭包传递给该方法。

Class.new()代替class。用Module#define_method()方法来代替def

1
2
3
4
5
6
7
8
9
my_var = "success"

MyClass = Class.new do
  puts my_var

  define_method :my_method do
    puts my_var
  end
end

使用方法来代替作用域门,那么可以让一个作用域看到另一个作用域中的变量,称为嵌套文法作用域(nested lexical scopes),简称扁平作用域(Flat Scope)

Shared Scope

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def define_methods
  shared = 0

  Kernel.send :define_method, :counter do
    shared
  end

  Kernel.send :define_method, :inc do |x|
    shared += x
  end
end

define_methods
counter # => 0
inc(4)
counter # => 4

instacne_eval()

1
2
3
4
5
6
7
8
9
10
11
class MyClass
  def initialize
    @v = 1
  end
end

obj = MyClass.new
obj.instance_eval do
  self # => #<MyClass:0x3340dc @v=1>
  @v   # => 1
end

在运行时,该块的接收者会成为self,因此它可以访问接收者的私有方法和实例变量。

1
2
3
v = 2
obj.instance_eval { @v = v}
obj.instance_eval { @v }

可以把传递给instance_eval()方法的块称为上下文探针(Context Probe),它就像是一个深入到对象中的代码片段,对其进行操作。

Breaking Encapsulation

1
2
obj = Object.new
obj.instance_eval { @options = Object.new }

当然也可以创建一个完整的类,先在initialize()方法中定义@options变量,然后创建该类的一个实例。

Clean Rooms

有时你会创建一个对象,仅仅是为了在其中执行块。这样的对象称为洁净室(Clean Room)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CleanRoom
  def complex_calculation
  end

  def do_something
  end
end

clean_room = CleanRoom.new
clean_room.instance_eval do
  if complex_calculation > 10
    do_something
  end
end

Callable Objects

从底层来看,使用块需要分两步。第一步,把代码打包备用;第二部,调用块(通过yield语句)来执行代码。

有以下三种方法可以打包代码。

  • proc。 pro基本上就是一个有块转换成的对象。
  • lambda。 它是proc的近亲。
  • 使用方法。

Proc Objects

一个Pro就是一个转换成对象的块,可以通过把块传给Prow.new()方法来创建一个Proc。以后就可以用Proc#call()方法来执行这个由块转换来的对象。这种技术称为延迟执行(Deferred Evaluation)

1
2
inc = Proc.new { |x| x+1 }
inc.call(2) #=>3

也可以用lambda()proc()创建Proc。

&操作符

把块转换成Proc

1
2
3
4
5
6
7
8
9
10
def my_method(&the_proc)
  the_proc
end

p = my_method { |name| "Hello, #{name}!" }
puts p.class
puts p.call("Bill")

=> Proc
   Hello, Bill!

Proc转换成块。

1
2
3
4
5
6
7
8
de my_method(greeting)
  puts "#{greeting}, #{yield}"
end

my_proc = proc { "Bill" }
my_method("Hello", &my_pro)

=> Hello, Bill!

Procs vs Lambdas

return

lambda中,return仅仅表示从 这个lambda中返回。

1
2
3
4
5
6
def double(callable_object)
  callable_object.call * 2
end

l = lambda { return 10 }
double(l) # => 20

proc中,return从定义proc的作用域中返回。

1
2
3
4
5
6
7
def another_double
  p = Proc.new { return 10 }
  result = p.call
  return result * 2 # unreachable code!
end

another_double # => 10

参数数量

1
2
p = Proc.new { |a,b| [a,b] }
p.arity # => 2

lambda参数数量不对时,会抛出ArgumentError。而proc会把传递进来的参数调整为自己期望的参数形式。

1
2
3
p = Proc.new { |a, b| [a,b] }
p.call(1,2,3) # => [1, 2]
p.call(1)     # => [1, nil]

Quiz: A Better DSL

定义事件,并在每个事件前运行所有的setup。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
event "the sky is falling" do
  @sky_height < 300
end

event "it's getting closer" do
  @sky_height < @mountains_height
end

setup do
  puts "Setting up sky"
  @sky_height = 100
end

setup do
  puts "Setting up mountains"
  @mountains_height = 200
end

Soulution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
lambda {
  setups = []
  events = {}

  Kernel.send :define_method, :event do |name, &block|
    events[name] = block
  end

  Kernel.send :define_method, :setup do |&block|
    setups << block
  end

  Kernel.send :define_method, :each_event do |&block|
    events.each_pair do |name, event|
      block.call name, event
    end
  end

  Kernel.send :define_method, :each_setup do |&block|
    setups.each do |setup|
      block.call setup
    end
  end
}.call

Dir.glob('*events.rb').each do |file|
  load file
  each_event do |name, event|
    env = Object.new
    each_setup do |setup|
      env.instance_eval &setup
    end
    puts "ALERT: #{name}" if env.instance_eval &event
  end
end

IV. Class Definitions

Inside Class Definitions

1
2
3
4
5
result = class MyClass
  self
end

result # => MyClass

在类(或模块)定义时,类本身充当了当前对象self的角色。

The Current Class

每当通过class关键字来打开一个类(或用module关键字打开一个模块),这个类就成为当前类。 使用class_eval()修改当前类。 Module#class_eval()方法(或者它的别名module_eval())会在一个已存在类的上下文执行一个块:

1
2
3
4
5
6
7
8
def add_method_to(a_class)
  a_class.class_eval do
    def m; 'Hello!'; end
  end
end

add_method_to String
"abc".m # => 'Hello!'

Module#class_eval()方法会修改self和当前类。 Object#instance_eval()方法会修改self和接收者的eigenclass。

Class Instance Variables

在类定义的时候,self的角色由类本身担任,因此实例变量@my_var属于这个类。 类的实例变量不同于类的对象的实例变量。

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass
  @my_var = 1

  def self.read; @my_var; end
  def write; @my_var = 2; end
  def read; @my_var; end
end

obj = MyClass.new
obj.write
obj.read            # => 2
MyClass.read        # => 1

上面定义了两个实例变量,都叫@my_var,但它们分属于不同的作用域,并属于不同的对象。 MyClass的实例变量也叫类实例变量

类变量

可以被子类或类的实例所使用。

1
2
3
4
5
6
7
8
9
class C
  @vv = 1
end

class D < C
  def my_method; @@v; end
end

D.new.my_method  # => 1
1
2
3
4
5
6
7
@@v = 1

class MyClass
  @@v = 2
end

@@v  # => 2

由于@@v定义于main的上下文,它属于main的类Object。MyClass集成自Object,因此它也共享了这个类变量。

Introducing Singleton Methods

1
2
3
4
5
6
7
8
9
str = "just a regular string"

def str.title?
  self.upcase == self
end

str.title?                    # => false
str.methods.grep(/title?/)    # => ["title?"]
str.singleton_methods         # => ["title?"]

只针对单个对象生效的方法,称为单件方法

The Truth About Class Methods

类只是对象,而类名只是常量。

1
2
an_object.a_method
AClass.a_class_method

类方法的实质就是:它们是一个类的单件方法。

单件方法的定义和类方法的定义是一样的。

1
2
def obj.a_singleton_method; end
def MyClass.another_class_method; end

在类定义中,由于self就是类本身,可以用self替换类名来定义类方法。

1
2
3
class MyClass
  def self.yet_another_class_method; end
end
1
2
def object.method
end

在上面的定义中,object可以是对象引用、常量类名或self。

Class Macros

Ruby对象并没有属性。如果希望由一些像属性的东西,就得定义两个拟态方法:一个读方法和一个写方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyClass
  def my_attribute=(value)
    @my_attribute = value
  end

  def my_attribute
    @my_attribute
  end
end

obj = MyClass.new
obj.my_attribute = 'x'
obj.my_attribute          # => 'x'

Module#attr_reader()可以生成一个读方法,Module#attr_writer()可以生成一个写方法,而Module#attr_accessor()可以同时生成两者

1
2
3
class MyClass
  attr_accessor :my_attribute
end

像attr_accessor()这样的方法称为类宏(Class Macro)。虽然类宏看起来像关键字,但是它们只是普通的方法,只是可以用在类定义中而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Book
  def title; end
  def lend_to(name); end

  def self.deprecate(old_method, new_method)
    define_method(old_method) do |*args, &block|
      warn "Warning: #{old_method}() is deprecated. Use #{new_method}()."
      send(new_method, *args, &block)
    end
  end

  deprecate :GetTitle, :title
  deprecate :LEND_TO_USER, :lend_to
end

b = Book.new
b.LEND_TO_USER("Bill")

=> Warning: LEND_TO_USER() is deprecated. Use lend_to().

Eigenclasses Revealed

Object#class()把eigenclass隐藏起来。

Ruby由一种特殊的基于class关键字的语法,它可以让你进入该eigenclass的作用域:

1
2
3
4
5
6
obj = Object.new
eigenclass = classs << obj
  self
end

eigenclass.class # => Class

每个eigenclass只有一个实例(这就是它们也称为单件类的原因),并且不能被继承。eigenclass是一个对象的单件方法的存活之所。

1
2
def obj.my_singleton_method; end
eigenclass.instance_methods.grep(/my_/)  # => ["obj.my_singleton_method"]

Method Lookup Revisited

Alt text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class C
  def a_method
    'C#a_method()'
  end
end

class D < C; end

obj = D.new
obj.a_method  # => 'C#a_method()'

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

class << obj
  def a_singleton_method
    'obj#a_singleton_method()'
  end
end

obj.eigenclass.superclass  # => D

C.eigenclass              # => #<Class:C>
D.eigenclass              # => #<Class:D>
D.eigenclass.superclass   # => #<Class:C>
C.eigenclass.superclass   # => #<Class:Object>

class C
  class << self
    def a_class_method
      'C.a_class_method()'
    end
  end
end

D.a_class_method          # => "C.a_class_method()"