Ruby 探索:Blocks 深入淺出

#Block #ruby
Johnson
技術文章
Ruby 探索:Blocks 深入淺出

圖片作者:Duncan WJ Palmer ,
來源連結

Ruby 探索:Blocks 深入淺出

在學習 Ruby 的過程中,Block 是其中很重要的一環,如果對 Block 有多一點的認識就能更了解一些深奧的 code 到底是什麼意思啦!這篇文章主要是在看完 Metaprogramming Ruby 2 中的 Block 章節後的讀書心得,以及一個我自己找的一個小題目來練習學到的觀念。


目錄

  • Scope 介紹
    • 什麼是 Scope ?
    • Scope Gate
  • Block 介紹
    • Block 從頭開始
    • Proc 基本介紹
    • procs 和 lambdas 怎麼分?!
    • & 符號解析
  • Block 實作
    • 自己做一個 before action
    • 第一步:試試看在 before 中執行 block
    • 第二步:在執行 before 的時候只有紀錄…
    • 第三步:重新定義要執行 before 的 method
    • 第四部:只有第一次執行 execute_before
  • 結論

Scope 介紹

什麼是 Scope ?

在介紹 Block 之前,我們需要先認識 ruby 中的 scope。
誒!什麼是 scope 呢?大概就是哪個變數可以在哪邊被看到的規則,舉例來說

x = 10
def method
  puts x
end

method

執行上面這段程式後會發生...

test.rb:3:in 'method': undefined local variable or method 'x' for main:Object (NameError)
    from test.rb:6:in '<main>'

在 method 方法內和外是不同的兩個 scope,所以 method 方法不能看到它外面的變數 x。如果想知道在自己的 scope 中可以使用哪些變數可以用 local_variables 來看!以下面這段 code 為例:

v1 = 1
class MyClass
  v2 = 2
  local_variables #[:v2]
  def my_method
    v3 = 3
    local_variables #[:v3]
  end
  local_variables # [:v2]
end
local_variables # [:v1, :obj]
obj = MyClass.new
obj.my_method

local_variables 會回傳現在的 scope 中可以看到的區域變數

在上面的例子我們可以發現,定義 class MyClassend 中間這段區域看不到變數 v1 ,同時 def my_method 到它的 end 這段也看不到變數 v2,這是因為他們在不同的 scope。

Scope Gates

在 Ruby 中有三個 Scope Gates:Class definitions, Module definitions 還有 Methods 他們就像一道牆,牆外牆內彼此看不見那裡有哪些變數。

當然也是有一些方法可以突破這樣子的限制,可以換個方式定義 Class 還有 Methods 讓他們不再是 Scope Gates,這方法叫做 Flattening the Scope (其實方法的名字好像也不是很重要),反正就是突破那個 scope 的限制啦!

my_var = "Success"
MyClass = Class.new do
  puts "#{my_var} in the class definition"
  define_method :my_method do
    puts "#{my_var} in the method"
  end
end

MyClass.new.my_method

可以成功印出

Success in the class definition
Success in the method

Block 介紹

Block 從頭開始

除了上述方法可以突破 scope 的限制外,其實 block 也是可以做到類似的效果的,它讓我們可以先寫一段程式,然後再方法裡面呼叫,就像這樣

def my_method
  x = "Goodbye"
  yield("cruel")
end

x = "Hello"

my_method do |y| 
  puts "#{x}, #{y} world" 
end 

會印出

Hello, cruel world

上面的例子,在呼叫 my_method 的時候我們傳了一個 block (do 到 end 的那段),大概可以想像他是 my_method 這方法的一個參數,接著 my_method 可以用 yield 這方法來執行這個 block。這個 block 厲害的地方在於它可以帶著當下的 bindings(註解1) 到 method 裡面去執行,所以在 my_method 可以看到 x 變數內的值是 Hello。

註解1

bindings 指的是當下 scope 可以使用的 variable, methos 還有 self

在 Ruby 的世界裡 block 是個特殊的成員,他不是物件也不能單獨存在,但其實還是有方法可以把 block 轉成物件讓我們處理的,那就是 Proc。

Proc 基本介紹

inc = Proc.new {|x| x + 1 }
inc.call(2) # 回傳 3

在 Proc.new 後面接一個 block 就可以產生一個 Proc 的物件,接著可以使用 call 方法來呼叫。有很多種方式可以產生 Proc 的物件,而且用不同方式產生的 Proc 也會不太一樣,首先來介紹 proc 還有 lambda ~

procs 和 lambdas 怎麼分?!

l1 = lambda {|x| x + 1 }
l2 = ->(x){ x + 1 }
p1 = Proc.new {|x| x + 1 }
p2 = proc {|x| x + 1 }

puts "l1: #{l1.lambda?}, #{l1.class}" # l1: true, Proc
puts "l2: #{l2.lambda?}, #{l2.class}" # l2: true, Proc
puts "p1: #{p1.lambda?}, #{p1.class}" # p1: false, Proc
puts "p2: #{p2.lambda?}, #{p2.class}" # p2: false, Proc

上面的 l1, l2, p1, p2 都可以使用 call 方法來執行,其中我們可以用 lambda? 來知道它是不是 lambda 如果不是那它就是 proc 啦!除了用 lambda->() 產生的 Proc 物件是 lambda 其他都是 proc 唷!

說了那麼久怎麼產生跟判斷 proc 和 lambda,當然就要來說他們有什麼不同啦!主要有兩個不同的地方,第一個是 return 的值,第二個是參數的判斷方式

procs 和 lambdas 有不同的 return
def double(callable_object)
  callable_object.call * 2
end

# return from lambdas
la = lambda { return 10 }

# return from the scope which defined the procs(main)
pr = proc { return 10 }

puts double(la) # 20
puts double(pr) # LocalJumpError...

執行程式後會

 20
 test.rb:12:in `block in <main>': unexpected return (LocalJumpError)
    from p92.rb:22:in `double'
    from p92.rb:29:in `<main>'

lambda 的 return 是從 lambda return,而 proc 則是從定義 proc 的 scope return 意思就是如果把 proc 用一個方法包起來的話...

def lambda_double
  la = lambda { return 10 }
  result = la.call
  return result * 2
end

def proc_double
  pr = Proc.new { return 10 }
  result = pr.call
  return result * 2  # unreachable code!
end

puts lambda_double # => 20
puts proc_double   # => 10

lambda_double 可以執行完方法中的每行程式,而 proc_double 則會在 pr.call 那行就 return 了,因為 proc 的 return 是從定義 proc 的地方也就是 proc_double 出發的。

procs 和 lambdas 處理參數的方式

procs 處理參數的方式比較有彈性,而 lambdas 則是比較嚴格,說起來不清楚!? 看 code 最快

puts "以下是 proc"
pr = proc {|a,b| [a,b]}
p pr.call('a', 'b')
p pr.call('a')
p pr.call('a', 'b', 'c')

puts "以下是 lambda"
la = lambda {|a,b| [a,b]}
p la.call('a', 'b')
p la.call('a')
p la.call('a', 'b', 'c')

執行程式後會

以下是 proc
["a", "b"]
["a", nil]
["a", "b"]
以下是 lambda
["a", "b"]
test.rb:8:in `block in <main>': wrong number of arguments (given 1, expected 2) (ArgumentError)
    from test.rb:10:in `<main>'

procs 遇到多的參數會自動丟掉,而不夠就補 nil,lambdas 則要求參數的數量正確才能執行。

& 符號解析

促使我想要多瞭解 ruby 中的 block 就是這個 & 符號了,第一次在 rails 專案裡面看到了這種符號真是既挫折又期待,覺得有很多東西可以學啦!在 C 語言中 & 是來拿記憶體位址的符號,在 ruby 裡面 & 符號到底是拿來做什麼的呢?

& 符號可以把 block 轉成 Proc 或是轉回來,多說無益看 code 就知道

def second_method
  puts 'In second_method'
  yield
end

def first_method(&operator) # 這裡的 & 是把 block 轉成 Proc 
  puts 'In first_method'
  operator.call
  second_method(&operator) # 這裡的 & 是把 Proc 轉成 block
end

first_method do 
  puts "this is block"
end

執行後會印出

In first_method
this is block
In second_method
this is block

在呼叫 first_method 得時候我們傳了一個 block 給他,我們可以把 block 想像成是最後一個參數,只是要在定義 first_method 的時候跟他說我們會有一個參數而且他是一個 block,因此就寫 &operator,之後就能在 first_method 中使用 operator 這個 Proc 啦~

如果想要不是馬上要用這個 Proc 還能把他先轉成 block 再傳給下一個 method,這時候就是在 operator 前加個 & 它就又從 Proc 轉成 block 囉!所以 second_method 使用 yield 才能成功呀!

以上就是關於 Block 的基本介紹啦,接下來做個小小的練習來應用一下學到的知識啦!

Block 實作:

自己做一個 before action

目標是讓下面的程式碼

class Example 
  extend BeforeAction
  before :method_a, :method_b do
    puts 'the code before method'
  end

  def method_a
    puts 'this is method a'
  end

  def method_b
    puts 'this is method b'
  end
end

instance_1 = Example.new
instance_1.method_a
instance_2 = Example.new
instance_2.method_b

可以有這樣的執行結果

the code before method
this is method a
the code before method
this is method b

要做一個 BeforeAction 的 Module ,在裡面有個 before 方法可以讓指定的 method 在執行之前,先執行 before 後面的 block

第一步:試試看在 before 中執行 block

module BeforeAction
  def before(*methods, &block)
    methods.each do |method| 
      block.call
    end
  end
end

現在我們可以依據定義了幾個 method 給 before 來執行幾次 block

第二步:在執行 before 的時候只有紀錄...

要做到 before action 就必須覆寫裡面的 method_a, method_b 之類的,但是在執行 before 的時候根本還沒有定義 method_a 是什麼,所以在這邊只能先把要執行的 block 還有要覆寫的 method 名字存起來!

module BeforeAction
  def new
    execute_before
    super
  end

  def before(*methods, &block)
    # 先存起來之後用
    @methods = methods
    @block = block
  end

  def execute_before
    # to do ...
  end
end

存起來之後我們可以在 Example 用了 new 方法時,再來處理我們的 block,讓那些需要執行 before action 的 method 可以被覆寫

第三步:重新定義要執行 before 的 method

module BeforeAction
  ...
  def execute_before
    @methods.each do |method| 
      # 幫原本的 method 取一個新名字
      newname = "new_#{method}"
      alias_method newname, method
      block = @block
      # 重新定義每個 method 要做什麼
      define_method method do 
        # 執行 block
        block.call
        # 原本的 method
        send(newname)
      end
    end
  end
end

這個步驟比較麻煩一點,重新定義方法後就會把原本的方法全部蓋過去。所以我們幫原本的方法取了個新名字(newname),在重新定義方法後就可以在呼叫(send)來執行原本方法的程式了!

到目前為止,我們的程式已經可以讓 Example 這個 class 做這樣的事囉!如果只有 new 一次都能正常執行~

instance_1 = Example.new
instance_1.method_a
instance_1.method_b

執行結果為

the code before method
this is method a
the code before method
this is method b

第四部:只有第一次執行 execute_before

因為只有在第一次 new 的時候需要執行 execute_before,所以我們可以做一點小小的修改...

module BeforeAction
  def new
    execute_before if first_time?
    super
  end

  def first_time?
    return false if @not_first_time
    @not_first_time = true
  end

  ...
end

最後:大功告成

完整程式碼如下

module BeforeAction
  def new
    # 只有第一次執行 new 時要 execute_before
    execute_before if first_time?
    super
  end

  def first_time?
    return false if @not_first_time
    @not_first_time = true
  end

  def before(*methods, &block)
    # 先存起來之後用
    @methods = methods
    @block = block
  end

  def execute_before
    @methods.each do |method| 
      # 幫原本的 method 取一個新名字
      newname = "new_#{method}"
      alias_method newname, method
      block = @block
      # 重新定義每個 method 要做什麼
      define_method method do 
        # 執行 block
        block.call
        # 原本的 method
        send(newname)
      end
    end
  end
end

class Example 
  extend BeforeAction
  before :method_a, :method_b do
    puts 'the code before method'
  end

  def method_a
    puts 'this is method a'
  end

  def method_b
    puts 'this is method b'
  end
end

instance_1 = Example.new
instance_1.method_a
instance_2 = Example.new
instance_2.method_b

執行後結果

the code before method
this is method a
the code before method
this is method b

在這範例練習到把 block 轉成 Proc ,然後存起來之後使用,在每次呼叫方法的時候都先執行這個 Proc

結論

透過寫這篇文章好好的整理了 block 相關的觀念,不然以前總是略懂略懂,但要回想的時候又要翻書查看半天...雖然這些東西可能不是馬上需要用到,但以後如果有需要用到這些技巧的時候,再回來看這篇自己寫的文章應該可以很快回憶起來吧!


👩‍🏫 課務小幫手:

✨ 想掌握 Ruby on Rails 觀念和原理嗎?

我們有開設 🏓 Ruby on Rails 實戰課程 課程唷 ❤️️