分享

Meta Programming - Ruby-tw

 weicat 2007-01-11

Metaprogramming 和 Ruby

把你的 programming Language 特化成專門對你要處理的 Domain 的語言( Domain Specifi Language),然後再用這種語言去處理你的問題。

著名的 Lisp 駭客 Paul Graham 在駭客與畫家這本書中說:

   In Lisp, you don‘t just write your program down toward the language. you also build the language up toward your program.

Ruby 也同樣的有這樣的特性。

getter & setter

最簡單的 Metaprogramming in Ruby,就是 attr_reader, attr_writer, attr_accessor。在 Ruby 裡所有的 instance variable 都是必須用 getter 和 setter 來存取。attr_xxxx 就是用來生成這些 getter 和 setter 的。

例如:

class MyClass
attr_accessor :nice
end
a = MyClass.new
a.nice = 100
puts a.nice

就等同於

class MyClass
def nice               #getter for instance variable nice
@nice
end
def nice=(num)    #setter for instance variable nice
@nice = num
end
end
a = MyClass.new
a.nice = 100
puts a.nice

這裡有幾個要說明的地方

  1. . Ruby 中 instance variable ,前頭都有個 @
  2. . getter 和 setter 的名字和 instance variable一樣
  3. . method 的名字後面有 = 表示他是 setter,於是在碰到 = 時就會去 call 這個 method
  4. . attr_accessor 並不是什麼神奇的 syntax suger,而只是單純的一個 method 罷了。
  5. . 在 Ruby 中, Class 的定義中也可以有 method call,所以才可以 call attr_accessor

自己寫個 attr_xxxx

我們也可以自己寫出類似這樣的 method。比如我們常常有些 instance variable 是一個 flag,在 Ruby 中會傳回真假值的 method 通常以 ? 做結尾。現在希望 Ruby 幫我們自動生成這些以 ? 結尾的 method,就是要能辦到像下面這樣:

class Work
attr_flag  :done ,:start
end
w = Work.new
puts "Yes, the work is started" if w.start?
puts "No, the work is not done"  if !w.done?

attr_flag 的寫法如下:

class Module
def attr_flag(*args)
args.each  do |sym|
class_eval %{
def #{sym}?
@#{sym}
end
}
end
end
end
  1. . class Module 表示我們把 Module 這個 class 又打開來加上一些新的定義。在 Ruby 所有的 class 都可以打開再加入新的定義。
  2. . 所有的 class 都是 class Module 的 instance ,所以這裡我們幫 Module 加上一個新的 instance method "attr_flag",所以他會變成所有的 class 的 class method 。
  3. . args 是一個陣列,內容是所有傳進來的參數,就是哪些 instance variable 是 flag。 args.each 是走過陣列的每一個元素(請參考 這篇)
  4. . class_eval 的意思就是把後面的這個字串(用 %{} 括起來的是字串),當做是目前這個 class 的內容。
  5. . 最後面用字串代換作出程式碼

同樣的功能用 define_method 也可以做到

class Module
def attr_flag(*args)
args.each  do |sym|
define_method("#{sym}?") { instance_variable_get "@#{sym}"}
end
end
end

用 define_method 好像比較簡潔,但是兩種方法都有人用。

method overload 的例子

ruby 本身並沒有提供 method overload 的功能。因為 Ruby 是一個 dynamic type 的語言,并具有duck typing 所以不需要像 method overload 这样的功能。不過因為 Ruby 是非常動態的語言,幫他加上 overload 的功能其實很簡單。

先來看看最我們要達到什麼效果:

class Test
# 這是我們要寫的 OverLoad Module
include OverLoad
# 一般的 method 定義
def t1
puts ‘original t1‘
end
# 直接可以用 block 來 overload t1 這個函數
# 第一個參數是要被 overload 的 method 名
# 接下來是任意個數的參數,用來指用 overload method 的參數型態(這裡是沒有指定)
# 最後是一個 block,也就是 method 的內容
overload :t1 do |a|
puts a
end
# 當然可以 overload 很多次
overload :t1 do |a,b|
puts a+b
end
# 這裡就有指定參數的型態了 (String, String)
overload :t1, String, String do |a,b|
puts "the String is #{a} #{b}"
end
end

下面就是 OverLoad Module:

module OverLoad
private
# 這是將 overload 所接到的「參數型態(spec)」轉成一個字串
# 這個字串在等一下動態產生程式碼的時候會用到
def self.spec_to_code(spec)
if Integer === spec
"args.length == #{spec}"
elsif Array === spec
i = -1
spec.map {|ts| "#{ts.inspect} === args[#{i+=1}]"}.join(" && ")
end
end
# included 是一個 Hook Method,每當有 class include 這個 module 時
# 這個 function 就會被呼叫
# include 這個 module 的 class 會被當成參數傳進來,也就是這裡的 the_class
def self.included(the_class)
#這裡幫 include 這個 module 的 class 加上一些新的 method
#the_class.class_eval { ooxx} 可以想成把 ooxx 直接寫在 the_class 的內容裡
the_class.class_eval do
#新加上的函式都是 private 的比較安全
private
# 加上 Overload_Dispetch_Db 這個 Hash
# 用來存 method_spec 和真的 method 的對映 (其實這裡存的不是 Method,而是 Proc)
self.const_set("Overload_Dispetch_Db", Hash.new)
# 加上 overload 這個 class method
# 以傳入的 [method名, 參數名字] 做為 key, 將 blok 存入 Hash 之中
def self.overload(method_id, *dispatch_spec, &block)
dd = self.const_get("Overload_Dispetch_Db")
if dispatch_spec ==[]
dd[[method_id,block.arity]] = block
else
dd[[method_id,dispatch_spec]] = block
end
# 把原來的 method 存起來
# 執行時若是在 Overload_Dispetch_Db 裡找不到要求的 method 的話
# 就會呼叫這個原來的這個 method
if method_defined?(method_id) && !method_defined?("old_#{method_id}")
alias_method "old_#{method_id}", method_id
end
# 動態的生成 method
body = ""
# 首先把 Hash 根據參數型態做排序
# 然後一個一個的產生程式碼存到 body 之中
# 這裡的排序是把有指定型態的排在前面
dd.sort do |a,b|
if  Array === a[0][1]
Array === b[0][1] ? (a[0][1].length <=> b[0][1].length) : -1
else
Array === b[0][1] ? 1 : a[0][1] <=> b[0][1]
end
end.each_with_index do |((mid,spec), target), i|
body<<<<-EOS
#{i == 0 ? ‘if‘ : ‘elsif‘} #{OverLoad.spec_to_code(spec)}
Overload_Dispetch_Db[[:#{mid},#{spec.inspect}]].call(*args)
EOS
end
method = <<-EOS
def #{method_id.to_s}(*args)
#{body}
else
if respond_to?:old_#{method_id}
old_#{method_id}(*args)
else
raise "Could not dispatch"
end
end
end
EOS
# 先把原來的 method undefine
# 再把做出來的 code 加到目前的 class 中
remove_method(method_id)
class_eval method
end
end
end
end

值得注意的是,在程式中的 Overload_Dispetch_Db 不能直接使用,要用 const_get 和 const_set 來存取。這是因為變數的 scope 的關係。在 class.eval 的 block 中,直接使用會被以為是 module OverLoad 的 Constant 而出現問題。

以之前舉的例子來看,最後 Test 裡的的 t1 會是這個樣子:

def t1(*args)
if String === args[0] && String === args[1]
Overload_Dispetch_Db[[:t1,[String, String]]].call(*args)
elsif args.length == 1
Overload_Dispetch_Db[[:t1,1]].call(*args)
elsif args.length == 2
Overload_Dispetch_Db[[:t1,2]].call(*args)
else
if respond_to?:old_t1
old_t1(*args)
else
raise "Could not dispatch"
end
end
end

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多