Chapter 4: Blocks - Part I

“Blocks are powerful tool for controlling scope, meaning which variables and methods can be seen by which lines of code.”― Metaprogramming Ruby ―

Blocks 應該是大家都很熟悉的主題,因為不管是初學者還是資深的工程師都很難不寫到 blocks。我對 blocks 剛開始的印象其實就是一段程式碼區塊,在 Ruby 編碼上就是 do…end 及 {… } 如此而已,但以書中的介紹來看,blocks 是非常強大的工具在控管變數、方法所影響的範圍。

Basics of Blocks

Blocks 有兩種寫法:

(1) { ... }: (通常的慣例是單行的程式碼區塊就會以 { … } 表示) 

# block 裡面的|num| 就是在程式碼中利用 yeild 方法傳遞過來的值

[1,2,3,4,5].select { |num| num.even? }  # => [2, 4]

(2) do … end: (超過一行的程式碼區塊就會以 do … end 表示)

Products.each do |product|
  if...
  else ...
  end
end

ㄧ個 Block 必須依附在 Method 後面,也就是說只有當你呼叫方法的時候,block 才會被執行。 就如以下程式碼來說,只有當使用 yield 時,方法才會開始啟動 block ,否則 block 就是只是ㄧ段不會被執行的程式碼而已。

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

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

判斷有沒有 block 附屬在方法上可以使用 block_given? 來詢問

def a_method
  return yield if block_given?
  "no block"
end

a_method                             # => "no block"

a_method { "you got a block" }       # => "you got a block"

Blocks Are Closures

Block 在其他語言中對應的概念是 closure。當ㄧ段能被執行的程式碼是需要兩個要素:

  1. CODE 程式碼本身 
  2. BINDINGS 連接 CODE 的變數、實例變數、等…

https://ithelp.ithome.com.tw/upload/images/20200929/2012086899f1ffSQ1Q.png

在Ruby的設計中,Block 就具備以上兩個特質,是一個隨時準備好可以執行的程式碼區塊。 看著上圖中 Bindings 的概念,再想想以下的範例,

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

x = "Hello"

my_method { |y| "#{x}, #{y} world" }     # => "Hello, Ruby world"

當我們建立 block{ |y| “#{x}, #{y} world”} 的時候也同時抓住了連接的 local bindings, 例如:x ( variable: “Hello”)。

接著把建立好的 block 傳進去方法裡,但是 my_method() 方法也有自己的 bindings 也是 x (variable: “Goodbye”),這個時候 block 看到自己的 bindings 已經就有 x 值了,而無視方法內的 x 值。

再來看看下面程式碼:

def just_yield
  # 方法內無法取得 top_level_variable 變數
  yield
end

top_level_variable = 1

# 在建立 block 的同時就會把 top_level_variable 抓進去local bindings裡面
just_yield do
  top_level_variable += 1
  local_to_block = 1       # 在local bindings的區域變數
end

top_level_variable         # => 2

local_to_block             # => "undefined local variable or..."

Scope

Scope (作用域,或稱為有效範圍) 就是指在該程式碼範圍內可以使用的變數、方法。 維基百科所定義的 scope:

在電腦程式設計中,作用域(scope,或譯作有效範圍)是名字(name)與實體(entity)的繫結(binding)保持有效的那部分電腦程式。不同的程式語言可能有不同的作用域和名字解析。而同一語言內也可能存在多種作用域,隨實體的類型變化而不同。作用域類別影響變數的繫結方式,根據語言使用靜態作用域還是動態作用域變數的取值可能會有不同的結果。― 維基百科 ―

先來看一段的程式碼:

name = "Kevin"
def introduce
  puts "Hi, my name is #{name}"  # => "undefined local variable ..."
end
# 會有錯誤訊息的原因是:方法內的 scope 是無法看到外面的變數。

Change of Scope

以下是書中的範例:

#  可使用 local_variables 取得當下區域變數的值
v1 = 1
self                       # main

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]
obj.my_method             # => [:v3]
local_variables           # => [:v1, :obj]˙

我們可從 self 得知當下的環境是 main,說明了 v1 是位於執行環境下的最頂層,也是在 top-level scope 裡的變數。

接著進入 MyClass 類別後,代表了進入了ㄧ個新的 scope (有效範圍),先前在 top-level scope 所定義的變數就不能再被使用。 在 MyClass 的有效範圍裡, 程式定義了 v2 以及 my_method() 方法。等到程式跑完 MyClass 類別後,才會又回到 top-level scope。

在此範例中,我們了解到不管程式中的 scope 如何改變,舊的 bindings 總是會隨著 scope 而被新的 bindings 取代。但 Global variable (全域變數) 則不在此限,因為 global variables 在任何一個 scope 都可以被使用。 這樣不被 scope 所限制的特質,卻也增加了日後除錯及維護的難度。