你看懂五倍紅寶石粉專上的 Ruby 版台灣共識了嗎?
最近「台灣共識」很熱門,公司的粉專也分享了 Ruby 版的台灣共識。
我們在公司內部的群組大家其實討論了蠻久,如果只是單純的去實作跟其他語言一樣的內容,不就沒有意義了嗎?
我們之所以會選擇用 Ruby 來當作工作上的工具,就表示他有一些特別的地方吸引我們。
所以,上面用了哪些 Ruby 技巧讓我們一起來分析看看!
先來看一下原始的版本,這是一個可以實際執行的 Ruby 語法。
def Consensus92(countries:, system:)
Module.new do
define_method 'definition' do
{ countries: countries, system: system }
end
define_method 'build_consensus_with?' do |other|
return true if definition == other.definition
raise "This is not #{other} consensus"
end
end
end
class Taiwan
extend Consensus92(countries: 2, system: 2)
end
class China
extend Consensus92(countries: 1, system: 2)
end
China.build_consensus_with?(Taiwan)
include 與 extend
大多數時候我們都是對 include
比較熟悉,因為它可以把一些方法切割到一個 Module 裡面,然後在物件中呼叫。
我們先來看一下這段程式碼:
module Extension
def echo
puts 'ECHO'
end
end
class A
include Extension
end
class B
extend Extension
end
p A.ancestors
# => [A, Extension, Object, Kernel, BasicObject]
A.new.echo
p B.ancestors
# => [B, Object, Kernel, BasicObject]
B.echo
我們會發現在 B
上面的繼承上,是沒有 Extension
模組的,所以兩者的差異在哪邊呢?
因為我們希望是
China.build_consensus_with?(Taiwan)
而不是China.new.build_consensus_with?(Taiwan.new)
的寫法,才選擇用extend
線索一
調查了 Ruby 的文件會發現 include
屬於 Module
物件的行為,而 extend
則是屬於 Object
物件的行為。
簡單說就表示 include
只能作用在 Class
上,物件的實例是不行的,像是下面這樣:
A.new.include Extension
但是 extend
是屬於 Object
的行為,所以原本我們預期是這樣
B.new.extend Extension
但是同時 Ruby 的所有東西都是物件的一種,所以同理可以證明 Module
也是一種物件(而 Class
物件繼承於 Module
)所以下面的用法也會成立:
B.extend Extension
線索二
根據 Ruby 文件提供的 extend
實作,大概是長這樣的,意外的很簡單。
static VALUE
rb_obj_extend(int argc, VALUE *argv, VALUE obj)
{
int i;
ID id_extend_object, id_extended;
CONST_ID(id_extend_object, "extend_object");
CONST_ID(id_extended, "extended");
rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS);
for (i = 0; i < argc; i++)
Check_Type(argv[i], T_MODULE);
while (argc--) {
rb_funcall(argv[argc], id_extend_object, 1, obj);
rb_funcall(argv[argc], id_extended, 1, obj);
}
return obj;
}
裡面其實就只是把 extend
傳入的 Module 都帶入,並且呼叫 extended
和 extend_object
兩個方法。
經過簡單的測試,像下面這樣的修改就能阻止 extend
複製方法。
module Extension
def self.extend_object(obj)
# Do Nothing
end
def echo
puts 'ECHO'
end
end
class B
extend Extension
end
B.echo
# => undefine method
也就是說,在 extend
的行為下,我們會透過 extend_object
方法做某些處理後,才得以「複製」方法,而不是像 include
一樣把整個 Module 放入物件的繼承體系之中。
因為文章篇幅限制,我們先不去追
extend_object
的源頭。
Consensus92 的用法
首先,大家可能會有點疑惑為什麼可以用 extend Consensus92()
這樣的寫法,我們先釐清一下「方法」和「常數」的差異。
def Consensus92; end
p Consensus92()
p Consensus92 # => uninitialized constant Consensus92
從上面這段程式碼我們可以發現,實際上「方法」和「常數」的命名空間是不同的,也就是說他們兩者並不衝突可以並存。而 Ruby 在這個情況下是透過有沒有 ()
來判斷到底是個方法,還是一個常數。
這邊省略 Ruby 的 Keyword Arguments 解釋,這部分雖然不常見但還是屬於日常使用的一部分。
那麼,為什麼 Consensus92()
的回傳結果可以被 extend
呢?
這個問題大家可能很快就猜到了,因為我們使用了「匿名模組」的技巧,雖然不確定是否真的有這個詞,不過大多數我們都用「匿名 XX」來稱呼一些沒有取名的定義,所以這邊也就這樣使用吧!
def Consensus92
Module.new; end
end
class Taiwan
extend Consensus92
end
因為不論 include
還是 extend
都只會確認對象是不是一個 Module 所以在這邊我們「即時」產生一個新的 Module 是符合 Ruby 在運作上的判定,也因此會被視為合法的行為。
所以實際上我們在 Taiwan
和 China
擴充的模組是不一樣的,這樣在程式的意義上,剛好也跟「九二共識沒有共識」的意思重疊在一起,畢竟從一開始「拓展(extend)」的共識就是不同的。
如果有在使用 Rails 的話,可能會注意到像是
Association_User_CollectionProxy
之類的類別名稱,其實就是運用這種技巧去動態產生的 Class 喔!
define_method 的理由
實際上,我們使用 Module.new do; end
和 module Extension; end
的效果是相同的,從下面的程式碼可以得到驗證:
A = Module.new
def echo
puts 'ECHO'
end
end
class B
extend A
end
B.echo
既然這樣也會運作,那麼我們為什麼還需要用 define_method
呢?
這是因為我們希望達到類似 Closure 的技巧,看看下面這段程式就會注意到一個有趣的問題:
def Consensus92(countries:)
Module.new
def definition
{ countries: countries }
end
end
end
class Taiwan
extend Consensus(countries: 2)
end
Taiwan.definition
# => undefine variable countries
為什麼會這樣,因為對 def
來說 countries
已經是屬於在執行階段的一部分,所以我們在呼叫 definition
的時候才會嘗試去尋找 countries
這個東西,但是他已經無法被找到。
但是 define_method
就不太一樣了!
def Consensus92(countries:)
Module.new do
define_method 'definition' do
{ countries: countries }
end
end
end
class Taiwan
extend Consensus92(countries: 2)
end
實際上 define_method
在被呼叫的當下,會被轉成像這樣的樣子
def definition
{ countries: 2 }
end
這是因為對於 define_method
所傳入的 Block 是用來定義方法的內容,但是因為我們是在呼叫一個方法,所以 countries
變數就被視為是處於 Consensus92
方法的環境下,而不是呼叫的當下。
稍微有點難懂,不過是不是很像 Closure 的感覺呢?
總結
這段程式碼其實算是有不少巧思在裡面,把程式碼換成中文讀起來意思也是很容易懂的。
def 九二共識(國家:, 制度:)
Module.new do
define_method '定義' do
{ 國家: 國家, 制度: 制度 }
end
define_method '建立共識?' do |定義|
return 是 如果 定義 == 對方.定義
raise "這不是#{other}共識"
end
end
end
class 台灣
擴充 九二共識(國家: 2, 制度: 2)
end
class 中國
擴充 九二共識(國家: 1, 制度: 2)
end
中國.建立共識?(台灣)
# => 錯誤「這不是台灣共識」
這也是 Ruby 在 DSL 表現優異上的原因之一,我們可以透過許多動態定義或者語法上的特殊技巧,製作出非常接近我們習慣的語言跟用法。
這篇文章提到關於 Ruby 類別上的應用,可以參考之前寫過的自由的 Ruby 類別來了解背後的機制。