如何讓Ruby代碼更簡練?!(原文最終修訂於 2006-08-18 下午02:42:25)

你可以用它來做什麼呢?請閱讀...
我四前年曾接觸過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
 

Robert C. Martin的英文blog網址: http://www.butunclebob.com/ArticleS.UncleBob 

譯者注:Robert C. MartinObject Mentor公司總裁,面向對象設計、模式、UML、敏捷方法學和極限編程領域內的資深顧問。他不僅是Jolt獲獎圖書《敏捷軟件開發:原則、模式與實踐》(中文版)(《敏捷軟件開發》(英文影印版))的作者,還是暢銷書Designing Object-Oriented C++ Applications Using the Booch Method的作者。MartinPattern Languages of Program Design 3More C++ Gems的主編,並與James Newkirk合著了XP in Practice。他是國際程序員大會上著名的發言人,並在C++ Report雜誌擔任過4年的編輯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章