avatar

大兜

右手寫程式,左手寫音樂

Ruby 2.0 重點介紹

終於也等到了 Ruby 2.0 RC 釋出的消息,看來正式版本也不遠了。而 Ruby 2.0 是什麼?又到底都參了啥好處?這裡整理一些我的筆記。

首先 Ruby 2.0 完全向下兼容,這是個好消息,意味之著你不用更改你的程式碼便可以從任何版本無痛升級 2.0(python 迷表示羨慕?),不用擔心你的 Ruby 專案(例如 Rails 等)升級後跑不動。故 Ruby 2.0 可看做是 Ruby 1.9 再加點料,而這些料可炫,且聽我道來。

Refinements

Module#refine

你過去擴展一個 class 可能會這樣做:

    "tonytonyjan".double
    # => NoMethodError: undefined method `double' for "tonytonyjan":String

    class String
      def double
        self*2
      end
    end

    "tonytonyjan".double # => "tonytonyjantonytonyjan"

Ruby 在團隊開發時,其中一項令人頭痛的問題是,當我們擴展某些 class 時,由於影響的是全局,萬一 method 撞名則會難以 debug。而現在 refinements 則可以將 class 擴展包在 namespace 裡面:

    module Foo
      refine String do
        def double
          self*2
        end
      end
      "tonytonyjan".double # => "tonytonyjantonytonyjan"
    end

    "tonytonyjan".double
    # => NoMethodError: undefined method `double' for "tonytonyjan":String

Kernel#using

寫好的擴展也可以拿出來給別人用:

    module Bar
      using Foo
      "test".double # => "testtest"
    end

    # in proc/lambda
    -> {
      using Foo
      "test".double # => "testtest"
    }.call

Keyword Arguments

寫過 python 大概對這不陌生,Ruby 2.0 也引進了一樣的功能,過去我們可能會在 method 參數中餵一個 Hash 當作 option 使用,通常會長這樣:

    def foo options = {}
      options = {a: "A", b: "B"}.merge options # given default value
      puts options[:a], options[:b]
    end

但在 Ruby 2.0,可以簡化如下:

    def foo a: "A", b: "B"
      puts a, b
    end

如果想拿到所有的 keys,就放兩顆星星:

    def foo(**options)
      p options
    end
    foo(a: "A", b: "B") # => {:a=>"A", :b=>"B"}
    foo(:a=>"A", :b=>"B") # => {:a=>"A", :b=>"B"}

Enumerator#lazy

以往我們可能要設個 timeout 或 counter 去列舉無窮 enumerator,但 Enumerator::Lazy 讓迭代過程這變的更為簡單:

    (1..Float::INFINATY).select(&:even?).take(3)      # 這會跑到海枯石爛
    (1..Float::INFINATY).lazy.select(&:even?).take(3) # 迭代每回合都會執行 `#event?`,而非數完所有元素

但不要看到新功能就高潮了,然後套用在每個地方,這方法通常不會比較有效率,除非真的有需要,否則少用為妙。

有興趣可以看看大師是怎麼用的

Module#prepend

談到擴展 class,新增 method 很容易,若想 override method 又想保留 origin 的功能卻很麻煩。

    class A
      def foo
        puts "A#foo"
        super
      end
    end

這時我們想要幫他的 #foo 加點料,又不想動到原來的程式(monkey patch),多會這麼做:

    module B
      def foo
        puts "B#foo"
      end
    end

    class A
      include B
    end

    A.new.foo

    # A#foo
    # B#foo

這是利用 A#foo 裡頭的 super 來完成,但寫 super 不應該是被擴展的方的責任,可以話,我們希望把 super 給拿掉。因此 Rails 曾在 active_support 下擴展 Module#alias_method_chian,但因為被報出問題不被建議使用,Rails 3 之後建議利用 Ruby 本身的 modulesuper 特性解決問題:

    class A
      module Base
        def foo
          puts "Base"
        end
      end

      module Ext
        def foo
          puts "Ext"
          super
        end
      end

      include Base
      include Ext
    end

    A.new.foo

    # => Ext
    # Base

理論上一個 module 透過 inclusion 不可能 override class method,因為 module inclusion 的運作是對 class 做 subclassing,這意謂著 class A 若 include module B,則 B 會變成 A 的爸爸,superclass 無法 override subclass method(總之爸爸無法改變女兒的行為,嘛,至少程式裡面是這樣)。這也是為什麼當初 Rails 會有 Module#alias_method_chian 的原因。

Ruby 2.0 中的 Module#prepend 就是為了解決此問題而生,取代 Module#alias_method_chian

    class A
      def foo
        puts "A"
      end
    end

    module Ext
      def foo
        super
        puts "Ext"
      end
    end

    class A
      prepend Ext
    end

    A.new.foo

    # => A
    # Ext

%i:Array of Symbol

p %w{hurray huzzah whoop}   # => ["hurray", "huzzah", "whoop"]
p %i{hurray huzzah whoop}   # => [:hurray, :huzzah, :whoop]