Metaprogramming Ruby (3) Block and Scope

基本概念

{ ... }do ... end 都是程式碼區塊 block, 本身不能當作物件。

基本的 block 用法,可以在 method 中插入 yield

def a_method(a, b)
  a + yield(a, b)
end

a_method(1, 2) {|x, y|(x + y) * 3 } #=>10

使用 Kernel#block_given? 來判斷有沒有接收到 block

def a_method
  return yield if block_given?
end

block 就是閉包

以最近流行的語言來說,在 javascript 可能比較會看到閉包這個概念,而早期的 Scheme,到 functional language 如 ML 和 LISP 也會大量運用。而 Ruby 為了向 LISP 致敬,也引入了這個概念。

本質上來說,一個閉包是一塊代碼,它們能作為參數傳遞給一個方法調用 - Martin Fowler
(全文可以看這裏

而達到閉包的好處,就是 block 在宣告的時候可以綁定語彙環境變數,然後交由 method 調用
以下面的例子來說:

def just_yield
  yield
  # 這邊不可以取用變數 top_level_variable
end

top_level_variable = 1

just_yield do 
  top_level_variable += 1  # 這個是語彙環境中的變數,會被包在 block 中,整包一起丟到 just_yield 中使用
  local_to_block = 1  # 這是 block 定義的 local 變數。
end # 當 block 被丟進去 method 中,這段程式碼就會執行了


top_level_variable # => 2  
local_to_block     # => Error! 因為 這個是閉包內的 local 變數
  • do ... end 的 block 內是個閉包環境,但他可以使用語彙環境內的變數,也就是 top_level_variable
  • 當這個 block 丟給 just_yield ,程式碼就會執行,top_level_variable 就會加一。
  • 但因為 block 是閉包,所以在 just_yield 中不可以使用 block 內定義的變數
  • 而跳出了 block ,語彙環境內的變數(top_level_variable)會被修改,但是 local 變數是不能取用的(local_to_block

總歸一句:block 就是個閉包,他可以攜帶所在的 local binding 進入 method 內「辦事」

Ruby 的 Scope

Ruby 的程式碼作用域跟 Java C# 不一樣:在內層的 scope 是看不到 outer scope 的區域變數。

a = 'yes'
def scope_test
    puts a  #  error, 看不到a
end

而更換 scope 就是更換區域變數的綁定 (binding)
使用 local_variables 可以查看目前所綁定的區域變數

v1 = 1
class MyClass
  v2 = 2
  local_variables #[v2]
  def my_method
    v3 = 3
    local_variables
  end
  local_variables #[v2]
end

obj = MyClass.new
obj.my_method     # =>[:v3]
local_variables   # =>[:v1, :obj]

如果要使用全域變數,可以使用錢字號 $來命名
但是因為難以追蹤影響的範圍,不建議這樣用。
盡量使用 instance variable @var,則可以在最上層定義這個變數,在內層的 scope 都可以讀到。

self 不同 scope 也一定不同

在不同的 object 內,self 必然也是不一樣的。

class MyClass
  def my_method(num)
    @var = num
    self
  end
  def my_other_method
    @var
  end
end
obj = MyClass.new
obj_1 = MyClass.new
obj.my_method(1)  # obj
obj_1.my_method(2)  # obj_1

obj.my_other_method  # 1
obj_1.my_other_method # 2

在 obj 與 obj_1 執行 my_method,也就更換了 self 的綁定,其實也就屬於不同的 scope。

Scope Gates

在 ruby 中 scope 像是一個閘門,裡外區域變數是無法互通的,主要的 scope gates 有三個:

  • Class definitions 類別定義的區塊
  • Module definitions 模組定義的區塊
  • Methods 方法定義的區塊

(其中 Class 和 Module 中的程式碼是立即執行的,但是 method 的 def 裡面的程式碼則是呼叫時才會執行。)

nested lexical scopes / flat scope

透過 block ,可以讓區域綁定的變數通過 scope gates。
例如以下的例子,利用 block 的方式來定義 class,就可以用到 local binding 的變數們。而像是 method 可以使用 define_method 的方式加上來加上 block,也可以達到使用 local binding 的效果:

my_var = "Success"
MyClass = Class.new do 
  puts " #{my_var} in the class definition!"
  #def my_method
  
  # 用 #defind_method 來呼叫block, 才可以帶入my_var
  defind_method :my_method do 
    " #{my_var} in the method"
  end
end

上面這個技巧就稱作 nested lexical scopesflat scope

也可以使用這個方法,來共享 scope:

def define_methods
  share = 0
  Kernel.send :defind_method, :counter do
    shared
  end
  Kernel.send :defind_method, :inc do |x|
    shared += x
  end
  
end

總結,如果要使用 flat scope 你可以用以下的方式改寫定義

  • class 定義改成 Class.new
  • module 定義改成 Module.new
  • method 定義改成 define_method

instance_eval()

instance_eval() 可以重新打開 instance scope

class MyClass
  def initialize
    @v = 1
  end
end

obj = MyClass.new

obj.instance_eval do 
  self    # MyClass
  @v      # =>1
end

但有沒有發現,打開之後的 self 還是原本的 MyClass,並不能使用呼叫位置的 local binding,因此如果要在這個狀況下使用 Flat Scope,可以使用 instance_exec() 這個方法,但實務上會破壞封裝,所以不太建議這樣做。

Proc

Proc 讓 block 轉化成物件的形式使用。使用 call 可以執行 proc

inc = Proc.new{|x| x + 1 }
inc.call(2)

或是使用 lambda。lambda 本身是 Proc 的一個實例(但會有些許不同,稍後會講)

dec = lambda {|x| x + 1}
dec.class
dec.call(2)
# 或是用
dec = ->(x){ x + 1 }

Proc 與 block 的轉換

使用 & 可以將 block 與 proc 互相轉換

def math(a, b)
  yield(a, b)
end

def do_math(a, b, &operation)
  # operation 是 proc
  # &operation 是 block
  math(a, b, &operation)
end

do_math(2, 3){|x, y| x * y } # =>6

block 轉為 proc

def my_method(&the_proc)
  the_proc
end

p = my_method {|name| "Hello, #{name}!" }
p.class # => Proc
p.call("Bill")

proc 轉為 block

def my_method(greeting)
  "#{greeting}, #{yield}!"
end

my_proc = proc { "Bill" }
my_method("Hello", &my_proc)

lambda 和 proc 的主要差別

lambda 會檢查參數的數量,proc不會

#lambda
lam = lambda { |x| puts x }
lam.call(2) #=> 2
lam.call(2,3) #=> ArgumentError

# proc
proc = Proc.new { |x| puts x }
proc.call(2) #=> 2
proc.call(2,3) #=> 2    proc忽略了其他參數,只傳第一個

return 的不同。lambda 的 return 會回傳給呼叫的地方,proc 的 return 會跳出整個 method

def lambda_test
  lam = lambda { return }
  lam.call
  puts "Haha! after lam call, i survived!"
end
def proc_test
  p = Proc.new { return }
  p.call
  puts "after proc call, i survived!"
end

lambda_test # => "Haha! after lam call, i survived!"
proc_test # => # 沒東西

建議使用 lambda,較為直觀。

Method Object

method 也可以被挑出來當做 callable object

class MyClass 
  def initialize(value)
    @x = value
  end
  def my_method
    @x
  end
end

object = MyClass.new(1)
m = object.method :my_method
m.call

obj#method 可以把 method 轉為 object (Singleton Method)

  • 可以把 Method 轉為 Proc ( Mehtod#to_proc)
  • 可以把 Block 轉為 Mthod (define_method)
    Method 和 Proc 的差別是:
  • method 內的 scope 是在它定義的 object 上
  • proc 內的 scope 是在定義的區域上

unbound method

method 可以使用下面兩種方式,脫離物件的綁定

  • Method#unbind
  • Module#instance_method
unbound = MyModule.instance_method(:my_method)
unbound.class # => UnboundMethod
  • Unbound form class 只能 bind 在同個 class 和 subclass
  • unbound form module 沒此限制
    可以用 define_method 去重新綁定
String.class_eval do 
  define_method :another_method, unbound
end
"abc".another_method # => 42

class_eval 可以重新打開 class)


總結

  • block 不是物件,但是可以轉換成物件 Proclamda
  • block 本身具有閉包的特性
  • Ruby 的 scope gate:
    • Class definitions 類別定義的區塊
    • Module definitions 模組定義的區塊
    • Methods 方法定義的區塊
  • Ruby 的 scope gate 蠻嚴謹的,跟其他語言不太一樣。要打破 scope gate,通常可以利用 block 閉包的特性來使用 flat scope 的技巧
  • method 也可以轉換成 method object 來使用,但使用上有限制。
  • instance_eval 可以把物件的 scope 重新打開,class_eval 可以把類別的 scope 重新打開。