如何讓Ruby程式碼更簡練?!(原文最終修訂於 2006-08-18 下午02:42:25)
阿新 • • 發佈:2019-02-06
你可以用它來做什麼呢?請閱讀...
我四前年曾接觸過Ruby,就是為了看看這個語言到底什麼樣。我用了它一段時間然後就把注意力放到Fit,Fitness(譯註1),和Java/.Net上了。然而最近,隨著Rails的興起,我又開始關注Ruby了;也開始認識到這是一個多麼高效、親和的語言。
學習一項事物最有效的還是通過實戰學習。所以我決定從一個Ruby的Kata(譯註2)開始,這樣就可以反覆去練習。我從Laurent Bossavit(譯註3)的blog裡挑出了哈利波特的Kata一篇。要解決這個問題中的某一塊,就涉及到一種能產生一個集合的所有可能組合的演算法。我在類庫中尋找能做這種組合演算法的模組,但只發現了一個做排列的傢伙。所以我決定自己寫一個。我覺得先寫一個組合迭代器的測試會比較有趣。
這裡就是使用rspec來寫的測試:
require 'spec'
require 'Combinations'
context "Simple Combinations" do
specify "degenerate cases" do
Combinations.get(0,0).should.equal []
Combinations.get(1,0).should.equal []
Combinations.get(0,1).should.equal []
end
specify "nC1" do
Combinations.get(1,1).should.equal [[0]]
Combinations.get(2,1).should.equal [[0],[1]]
end
specify "nCn" do
Combinations.get(2,2).should.equal [[0,1]]
Combinations.get(3,3).should.equal [[0,1,2]]
end
specify "nCr" do
Combinations.get(3,2).should.equal [[0,1],[0,2],[1,2]]
Combinations.get(4,3).should.equal [[0,1,2],[0,1,3],[0,2,3],[1,2,3]]
Combinations.get(5,3).should.equal [
[0,1,2],[0,1,3],[0,1,4],[0,2,3],[0,2,4],[0,3,4],
[1,2,3],[1,2,4],[1,3,4],
[2,3,4]
]
end
end
而這裡就是那些通過測試的組合模組:
我非常確定這個演算法正確且有效。可是,我赫然發現一個稍早的版本固然精確,但效率卻出奇的低。不同點在於:
(s...n).each {|i| combine(combination + [i], i+1, n, r-1, proc)}
令我煩心是,測試並沒有發現這個效率低下的問題。我是在之後所做的一些手工的探索測試中才發現了這點。我想應該把它放在一個單元測試中來確保一個特定的最低效率值。不過我會把它放在另一篇blog中。
這篇blog的真正主題
是我不喜歡這個程式碼,它太不清晰了。可是到目前為止我還沒找到一種讓它更好的辦法。我的目標是找到一種展現程式碼的方式,這樣這個演算法就能以明朗的面貌展現出來。我的這個演算法太擁擠了,而且不能把自己描述清楚。(如何判斷擁擠指數呢?就是讓你完全理解它在做什麼和為什麼它能通過測試所花的時間的多少。)
有什麼建議嗎?
譯者的話:因本文秉承Uncle Bob一貫的集思廣益風格,屬於一篇探討性blog。原blog中有大量的專家討論,譯者特還其以原貌(請見以下評論內容),讓大陸友人能夠汲取更多相關知識。
----------------------------------------------------------------------------------------------------------
評論之一:
來自-> Matteo Vaccari
題目-> 清理組合程式碼
這些事情最好用遞迴定義的方式來解決。
讓函式choose(n, k)來找出來自集合(0...n)的k個元素的所有不同組合。那麼
choose(3, 0) == [[]]
choose(3, 1) == [[0], [1], [2]]
choose(3, 2) == [[0,1], [0,2], [1,2]]
等等。我們也有
choose(3, 4) == []
因為沒辦法從僅僅三個元素中找出4種元素的不同組合。
所以,讓我們給choose(n, k)來寫一個遞迴的定義;基本的情況是
choose(0, 0) == [[]]
choose(0, k) == [] if k > 0
這覆蓋了所有n == 0 時的情況。現在讓我們看看n==3,
choose(3, 1) == [[0], [1], [2]]
choose(3, 2) == [[0,1], [0,2], [1,2]]
那當n==4時會怎樣呢?
choose(4, 2) == [[0,1], [0,2], [1,2], [0,3], [1,3], [2,3]]
酷!看起來前面一半和choose(3,2)一樣
choose(4, 2) == choose(3, 2) + [[0,3], [1,3], [2,3]]
剩下的元素與choose(3,1)再加上新元素3是一樣的 choose(4, 2) == choose(3, 2) + append_all(choose(3,1), 3)
這就說明這是一個普遍的規則。從一組(n+1)個元素元素中選出不同的k個元素的所有組合的方式是:
- 從一組n個元素中選出k個元素的所有組合,再加上
- k-1箇舊元素的所有組合加上一個新的元素 測試優先! def test_base
assert_equal [[]], choose(3, 0)
assert_equal [], choose(0, 3)
end def test_step
# choose(1,1) == choose(0, 1) + append_all(choose(0, 0), 0)
# == [] + append_all([[]], 0)
# == [[0]]
assert_equal [[0]], choose(1, 1)
assert_equal [[0,1], [0,2], [1,2]], choose(3, 2)
assert_equal [[0,1], [0,2], [1,2], [0,3], [1,3], [2,3]], choose(4, 2)
assert_equal [[0,1,2,3]], choose(4, 4)
end 通過測試的程式碼是 def choose(n, k)
return [[]] if n == 0 && k == 0
return [] if n == 0 && k > 0
return [[]] if n > 0 && k == 0
new_element = n-1
choose(n-1, k) + append_all(choose(n-1, k-1), new_element)
end def append_all(lists, element)
lists.map { |l| l << element }
end
既然我們遞迴呼叫k-1,我們必須增加一段程式碼去定義當k==0時的情況。這段程式碼當然是精簡的。它也是清晰的,只要你明白了遞迴定義是如何 奏效的。 評論之二: 來自-> Dean Wampler 題目-> 一個更“美化”的調整? 這裡是一個原始的rspec測試的調整,它試圖用更美化的方式來封裝Combinations.get()的呼叫,使用一個全域性方法: require 'spec'
require 'Combinations' def get_combinations args
Combinations.get args[:for_n_items], args[:sets_of]
end context "Simple Combinations" do
specify "degenerate cases" do
get_combinations(:sets_of => 0, :for_n_items => 0).should.equal []
get_combinations(:sets_of => 0, :for_n_items => 1).should.equal []
get_combinations(:sets_of => 1, :for_n_items => 0).should.equal []
end
specify "nC1" do
get_combinations(:sets_of => 1, :for_n_items => 1).should.equal [[0]]
get_combinations(:sets_of => 1, :for_n_items => 2).should.equal [[0],[1]]
end
specify "nCn" do
get_combinations(:sets_of => 2, :for_n_items => 2).should.equal [[0,1]]
get_combinations(:sets_of => 3, :for_n_items => 3).should.equal [[0,1,2]]
end
specify "nCr" do
get_combinations(:sets_of => 2, :for_n_items => 3).should.equal [[0,1],[0,2],[1,2]]
get_combinations(:sets_of => 3, :for_n_items => 4).should.equal [[0,1,2],[0,1,3],[0,2,3],[1,2,3]]
get_combinations(:sets_of => 3, :for_n_items => 5).should.equal [
[0,1,2],[0,1,3],[0,1,4],[0,2,3],[0,2,4],[0,3,4],
[1,2,3],[1,2,4],[1,3,4],
[2,3,4]
]
end end 如果經常用的化會顯得有些冗長,可是這對第一次使用的讀者來說可以更容易讀懂,而且也是個選擇。 ---------------------------------------------------------------------------------------------------------- 譯註: 1,Fit,Fitness,一個Object Mentor公司開發的關於驗收性測試的知名框架,詳情可訪問http://fitnesse.org/。 2,Kata,是目前北美和歐洲一些領先的軟體諮詢公司開創的一種用於掌握軟體開發技能的手段,類似於國人樂談的武功招式。目的就是試圖尋找出軟體開發中的一些招式,讓學習者可以不斷演練,從而打下一個良好的基礎。 3,Laurent Bossavit,敏捷領域的一位知名專家,並有熱門blog。
require 'Combinations'
context "Simple Combinations" do
specify "degenerate cases" do
Combinations.get(0,0).should.equal []
Combinations.get(1,0).should.equal []
Combinations.get(0,1).should.equal []
end
specify "nC1" do
Combinations.get(1,1).should.equal [[0]]
Combinations.get(2,1).should.equal [[0],[1]]
end
specify "nCn" do
Combinations.get(2,2).should.equal [[0,1]]
Combinations.get(3,3).should.equal [[0,1,2]]
end
specify "nCr" do
Combinations.get(3,2).should.equal [[0,1],[0,2],[1,2]]
Combinations.get(4,3).should.equal [[0,1,2],[0,1,3],[0,2,3],[1,2,3]]
Combinations.get(5,3).should.equal [
[0,1,2],[0,1,3],[0,1,4],[0,2,3],[0,2,4],[0,3,4],
[1,2,3],[1,2,4],[1,3,4],
[2,3,4]
]
end
end
讓函式choose(n, k)來找出來自集合(0...n)的k個元素的所有不同組合。那麼
choose(3, 0) == [[]]
choose(3, 1) == [[0], [1], [2]]
choose(3, 2) == [[0,1], [0,2], [1,2]]
choose(3, 4) == []
因為沒辦法從僅僅三個元素中找出4種元素的不同組合。
所以,讓我們給choose(n, k)來寫一個遞迴的定義;基本的情況是
choose(0, 0) == [[]]
choose(0, k) == [] if k > 0
這覆蓋了所有n == 0 時的情況。現在讓我們看看n==3,
choose(3, 1) == [[0], [1], [2]]
choose(3, 2) == [[0,1], [0,2], [1,2]]
那當n==4時會怎樣呢?
choose(4, 2) == [[0,1], [0,2], [1,2], [0,3], [1,3], [2,3]]
酷!看起來前面一半和choose(3,2)一樣
choose(4, 2) == choose(3, 2) + [[0,3], [1,3], [2,3]]
剩下的元素與choose(3,1)再加上新元素3是一樣的 choose(4, 2) == choose(3, 2) + append_all(choose(3,1), 3)
這就說明這是一個普遍的規則。從一組(n+1)個元素元素中選出不同的k個元素的所有組合的方式是:
- 從一組n個元素中選出k個元素的所有組合,再加上
- k-1箇舊元素的所有組合加上一個新的元素 測試優先! def test_base
assert_equal [[]], choose(3, 0)
assert_equal [], choose(0, 3)
end def test_step
# choose(1,1) == choose(0, 1) + append_all(choose(0, 0), 0)
# == [] + append_all([[]], 0)
# == [[0]]
assert_equal [[0]], choose(1, 1)
assert_equal [[0,1], [0,2], [1,2]], choose(3, 2)
assert_equal [[0,1], [0,2], [1,2], [0,3], [1,3], [2,3]], choose(4, 2)
assert_equal [[0,1,2,3]], choose(4, 4)
end 通過測試的程式碼是 def choose(n, k)
return [[]] if n == 0 && k == 0
return [] if n == 0 && k > 0
return [[]] if n > 0 && k == 0
new_element = n-1
choose(n-1, k) + append_all(choose(n-1, k-1), new_element)
end def append_all(lists, element)
lists.map { |l| l << element }
end
既然我們遞迴呼叫k-1,我們必須增加一段程式碼去定義當k==0時的情況。這段程式碼當然是精簡的。它也是清晰的,只要你明白了遞迴定義是如何 奏效的。 評論之二: 來自-> Dean Wampler 題目-> 一個更“美化”的調整? 這裡是一個原始的rspec測試的調整,它試圖用更美化的方式來封裝Combinations.get()的呼叫,使用一個全域性方法: require 'spec'
require 'Combinations' def get_combinations args
Combinations.get args[:for_n_items], args[:sets_of]
end context "Simple Combinations" do
specify "degenerate cases" do
get_combinations(:sets_of => 0, :for_n_items => 0).should.equal []
get_combinations(:sets_of => 0, :for_n_items => 1).should.equal []
get_combinations(:sets_of => 1, :for_n_items => 0).should.equal []
end
specify "nC1" do
get_combinations(:sets_of => 1, :for_n_items => 1).should.equal [[0]]
get_combinations(:sets_of => 1, :for_n_items => 2).should.equal [[0],[1]]
end
specify "nCn" do
get_combinations(:sets_of => 2, :for_n_items => 2).should.equal [[0,1]]
get_combinations(:sets_of => 3, :for_n_items => 3).should.equal [[0,1,2]]
end
specify "nCr" do
get_combinations(:sets_of => 2, :for_n_items => 3).should.equal [[0,1],[0,2],[1,2]]
get_combinations(:sets_of => 3, :for_n_items => 4).should.equal [[0,1,2],[0,1,3],[0,2,3],[1,2,3]]
get_combinations(:sets_of => 3, :for_n_items => 5).should.equal [
[0,1,2],[0,1,3],[0,1,4],[0,2,3],[0,2,4],[0,3,4],
[1,2,3],[1,2,4],[1,3,4],
[2,3,4]
]
end end 如果經常用的化會顯得有些冗長,可是這對第一次使用的讀者來說可以更容易讀懂,而且也是個選擇。 ---------------------------------------------------------------------------------------------------------- 譯註: 1,Fit,Fitness,一個Object Mentor公司開發的關於驗收性測試的知名框架,詳情可訪問http://fitnesse.org/。 2,Kata,是目前北美和歐洲一些領先的軟體諮詢公司開創的一種用於掌握軟體開發技能的手段,類似於國人樂談的武功招式。目的就是試圖尋找出軟體開發中的一些招式,讓學習者可以不斷演練,從而打下一個良好的基礎。 3,Laurent Bossavit,敏捷領域的一位知名專家,並有熱門blog。
譯者注:Robert C. Martin是Object Mentor公司總裁,面向物件設計、模式、UML、敏捷方法學和極限程式設計領域內的資深顧問。他不僅是Jolt獲獎圖書《敏捷軟體開發:原則、模式與實踐》(中文版)(《敏捷軟體開發》(英文影印版))的作者,還是暢銷書Designing Object-Oriented C++ Applications Using the Booch Method的作者。Martin是Pattern Languages of Program Design 3和More C++ Gems的主編,並與James Newkirk合著了XP in Practice。他是國際程式設計師大會上著名的發言人,並在C++ Report雜誌擔任過4年的編輯。