Metaprogramming in Programming Ruby

本篇文章是Programming Ruby第三部份關於meataprogramming的讀書筆記,幫助整理思路。

筆者的學習經驗中,直接進Metaprogramming Ruby感覺比較吃力,有一些卡關的觀念是看了這章後才豁然開朗的。如果讀者讀Metaprogramming Ruby碰壁,可以考慮先讀Programming Ruby的這一章。

Object and Class

除了block,Ruby中萬物皆物件。Ruby中沒有真正的類,Ruby的類宣告其實全等於初始化一個類別物件,並指派給一個常數。

class MyClass; end

# Behind the hood
MyClass = Class.new

對Ruby來說,類只是物件的一種而已。

Object包含 flag (object.object_id),實例變數,和關聯到的 class。

Class因為也是object,所以包含上面所有,再加上一些(實例)方法和關聯到的superclass。

因為類也是物件,所以我們常見到的class method其實不存在class中,而是存在其產生的singleton class中作為實例方法而存在。當我們為一個類新增class method,就會產生一個中繼class存放這個方法,並且當前的物件的類變成此中繼class。稍後會有更詳細的說明。

self的幾個用途

self指的是當前object,如果沒有explicit receiver,self不會變化。

  1. 用來控制Ruby找其實例變數。
  2. 當沒有explicit receiver時,會以self作為receiver。
  3. 當有explicit receiver時,self會被設定為該object。

self in class object

當使用class關鍵字時,會將self設為此class object,在class定義的上下文中,self總是被設為此class object。

Singletons

以下內容移至Singleton Classes。較進階,也較完整。

Module and Mixins

include是在class內才可以使用的方法。Include module目的是在class中增加一些instance methods。(但也可以增加class method,而且其實有被大量使用,所以不能從include判斷是否是增加instance method還是class method)在class裡include module就像是為這個class新增一個superclass,存放一些instance method。

實踐上,當include module時,Ruby會建一個新匿名的class object,當前class的superclass指向它,而新的匿名class object的superclass指向原class的superclass。此匿名class本身並不帶instance method,要作函數找查時,它會指向module要函數。如此才可以做到隨意插入module至class中而不會造成繼承樹的錯亂。但也有一個問題,就是一旦改動module,所有include此module的都會立即改變。

Ruby在物件的繼承鏈中只能被include一次。

prepend也是類似,只不過與原class的繼承關係相反。

extend是物件的方法,任意物件都可以使用。Extend module是針對特定object增加instance methods,因此不會被其他instance共享。

如果在class中extend module,則會創建class methods。因為class裡的self就是class本身。

簡言之,include和prepend會為class增加instance methods,而且其所有的instance皆可使用。相對的,extend直接對object增加instance method,最class則會增加class-level instance method (kind of class method),且只有該object可以使用。

Class-level Macros

這裡解釋attr_accessor, has_many這類的class method是如何實現的。

先從一般object開始。當我使用obj.method的時候,Ruby會將self設為obj,然後到obj所屬的class的instance methods中尋找,如果找不到,就再往其superclass中尋找,這中間當然也包括所有prepend和include的module。

對class而言亦然。在child class中呼叫class method時,如果不存在,它就會往其superclass找class method。

has_many和attr_accessor這類的magic就是這樣泡製出來的。我們只是只是輸入一些方法名,它就自動幫你生出相對應的函數。

書中舉例

class People
  def self.say_hi
    def say_hi(foo)
      "hihi " + foo
    end
  end
end

class Engineer < People
  say_hi
  # or self.say_hi("Yooo!")
end

Engineer.new.say_hi("xxx")

在Enginner裡執行 self.say_hi,self 是 Engineer,找不到該方法,所以會往其 parent 找,在 People 找到該方法並執行,因此 Engineer 可以得到一個 instance method,存在People。這在Engineer定義完成時就會生成(因為逐行執行,只有函數內的東西在生成時不會被執行)。往後 Engineer 的任何實例都可以使用實例方法 say_hi 了。

這樣就可以理解 has_many 這樣的類方法是怎麼使用的了。say_hi 換成 has_many,"Yooo!"換成 DB 欄位的參數,就可以讓新的 model 實例生成後就能使用一系列的實例方法了。

不過,當使用 has_many 時,我們是依據參數內容產生不同名的實例方法,這意味著要動態的生成方法,我們可以使用define_method 做到這點

module ActiveRecord
  class Base                           # 類比ActiveRecord::Base,實現has_many
    def self.has_many(things)
      define_method(things) do         # 如果即將定義的實例方法有參數,加在do的後面
        "There are your " + things
      end
    end
  end
end

class Factory < ActiveRecord::Base
  self.has_many "cars"
end

Factory.new.cars                       # 因為has_many後面接了cars,所有實例自動會有cars方法,這是在定義Factory時就產生的
#=> "There are your cars"              # 執行cars方法得到的結果

值得注意的一個細節是,define_method永遠只會產生instance_method,不論是在class_exec中或是instance_exec中。

Class Method and Modules

有更多時候,我們的class已經繼承其他class,因此無法再繼承其他class。這時可以把這些方法寫進module,使用extend module為class新增類方法,類方法的內容就類似上面的self.has_many。

如果同時有class method和instance method要加進一個class,不能用簡單的使用extend。這時我們會須要使用一個rails極常使用的pattern:ruby的hook method included,此方法在你include module到class時會自動調用

直接上demo code。

module MyModule
  # include 後會成為 instance method
  def my_instance_method
  end

  # 用 module 包住 class method,慣例上 module 名為 ClassMethods
  module ClassMethods
    # include後會成為class method
    def my_class_method
    end
  end

  # 當有 class include 此 module 時 extend 該 class
  def self.included(host_class)
    host_class.extend(ClassMethods)
  end
end

class Example
  include MyModule
end

Example.my_class_method
Example.new.my_instance_method

Two Other Forms of Class Definition

第一種

class A < B
end

B可以換成任何回傳一class object的東西,比方Struct.new會回傳一個class object,所以也可以寫成像

class A < Struct.new(:name, :age)
end

第二種

SomeClass = Class.new do
  def some_method
  end
end

SomeClass.new

instance_eval and class_eval

Object#instance_eval, Module#class_eval, Module#module_eval 讓我們可以將 self 設定為任意 object,執行 block 內容後,再將 self 指回原本的 object

"Yoo".instance_eval { self.upcase }
#=> "YOO"

# 定義 class method
class A; end
A.instance_eval do
  def say_hello
    puts "Hello!"
  end
end

A.say_hello

class_eval 也如同 instance_eval,只不過它多設定了可以定義方法的環境,就像 reopen class 一樣。

# 定義 instance method
class A; end
A.class_eval do
  def say_hi
    puts "Hi!"
  end
end
A.new.say_hi
#=> Hi!

Object#instance_exec, Module#class_exec, Module#module_exec 讓我們可以額外送進別的object當參數,放於 block 的參數中。

Constant lookup 在 eval 中是看 lexical 環境。

Hooked Methods

hooked methods == callbacks

被解釋器自動調用的方法。在 Ruby 中是以名稱去找的,如果你有定義同名函數,就會執行之。下表是書中所列的 hooked methods

inherited

在有別的 class 繼承於當前 class 時被調用,參數為 child class

class Parent
  def self.inherited(child)
    puts "Some class inherited from this"
  end
end

class Child < Parent
end

#=> Some class inherited from this

method_missing to simulate accessors

完整參數:def method_missing(name, *args, &block) ...

以下簡化書中範例,自己寫一個只讀的 OpenStruct,。繼承於 BasicObject 以獲得最少的實例方法。我附上註解。

class MyOpenStruct < BasicObject
  def initialize(initial_values = {})
    @values = initial_values    # MyOpenStruct的實例變數是一個hash
  end
  def _singleton_class
    class << self               # 新增一個MyOpenStruct的singleton class
      self                      # 並且回傳該singleton class
    end
  end

  def method_missing(name, *args, &block)
    _singleton_class.instance_exec(name) do |name|    # 找不到方法時,在singleton中新增實例方法
      define_method(name.to_sym) do
        @values[name]
      end
    end
    @values[name] + 3
  end
end

struct = MyOpenStruct.new(x:3)
struct.x  #=> 6   # 會傳到 method_missing
struct.x  #=> 3   # 第二次調用時,因為該方法在method_missing中已經被定義,所以直接調用該方法

繼承BasicObject看起來是使用method_missing的好方法,但是因為缺少很多常用方法,它帶來的壞處可能比好處更多。

method_missing as a filter

用method_missing去定義所有方法是不實際的。像ActiveRecord::Base是用regular expression去過濾方法,不符合的就會調用parent的method_missing。

results matching ""

    No results matching ""