iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 2
0
Modern Web

續說 Ruby on Rails系列 第 2

[Day 2] Ruby 的 closure 們: Block, Proc, lambda

記得剛進公司的時候,遇到 Proc 或 lambda 都會很疑惑,為什麼可以這樣用,為什麼不直接使用 block就好?直到越看越多資料後,才漸漸瞭解他們的區別跟用途。

Block

Block 是程式碼區塊,不是物件不能單獨存在,也不能指定給變數。
所以 Block 一定會跟隨在方法後面。 Ruby 的方法就像是其他語言的函式,那 block 就是函式內部的函式
一般來說我們把參數帶入函式,由函式決定要怎麼處理這個參數,有了block,我們讓參數也有邏輯。

Block 的兩種型態:

# i 是 block 的參數
[1, 2, 3].map { |i| p "這個矩陣依序是:#{i}" }

# 等同:
[1, 2, 3].map do |i|
  p "這個矩陣依序是:#{i}"
end

# 都會印出
"這個矩陣依序是:1"
"這個矩陣依序是:2"
"這個矩陣依序是:3"
# 回傳 ["這個矩陣依序是:1", "這個矩陣依序是:2", "這個矩陣依序是:3"]

用 {} 好,還是用 do ... end 好?

=> 如果 block 內容簡單,一行可以寫完,會用 { }。

什麼時候使用 Block => yield

在方法內部看到 yield,就是要把控制權讓出來,給外面的block

def hello_block(word)
  p "#{word}開始"
  yield
  p "#{word}結束"
end

hello_block('hello') { p 'block在此' }

# "hello開始"
# "block在此"
# "hello結束"

可以看到block沒有被視為參數,只有word才是參數。
如果沒有給 block,會噴錯: LocalJumpError: no block given (yield)
如果不是每次都給block,可以用 block_give? 這個方法,有 block 才執行 yield

def hello_block(word)
  p "#{word}開始"
  yield if block_given?
  p "#{word}結束"
end


hello_block('hello')  
# "hello開始"
# "hello結束"

如果沒有 yield,呼叫方法時block有給跟沒給一樣

'hello'.split('l')
# ["he", "", "o"]

'hello'.split('l') { p 'block' }
# ["he", "", "o"]

透過 yield(i),傳參數進 block

def filter_number(array)
  p "參數是#{array}"
  result = []
  array.each do |i|
    result << i if yield(i) 
  end
  result 
end


filter_number([*1..10]) { |i| i % 2 == 0}
# "參數是[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
#=> [2, 4, 6, 8, 10]

filter_number([*1..10]) { |i| i % 3 == 0}
# "參數是[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
# => [3, 6, 9]

因為 block 最後一行會作為回傳值,當回傳 true 才把參數塞進篩選結果

把 Block 放進參數

等等,前面不是說 block 不能當參數嗎?為什麼又可以了?不是我誆你,是使用了& 轉換。前面的範例在方法的參數列看不出來需不需要 block,因為他們是implicite block,現在要主動把 block 放進參數。

def email_customer(email_address, &log)
  puts "I will email #{email_address}."
  if block_given?
    log.call
  end
  # code to execute ...
  puts "I've emailed #{email_address}."
end

email_customer('a@c.com') { p "log the email record" }


# I will email a@c.com.
# "log the email record"
# I've emailed a@c.com.

透過&,把傳進來的log 轉換成 proc,這樣一來就可以在方法內用call執行 log 這個 proc。
因為有if block_given?,如果沒有給 log 也不會錯

email_customer('a@c.com')
# I will email a@c.com.
# I've emailed a@c.com.

題外話,用implicite block 會比 block 轉換 proc 效能更好一點。

Proc

proc 是 Proc class的實體,proc 可以指定給變數存起來,讓傳進方法的程式碼區塊更有變化性。

建立 proc 的兩個方法:

proc = proc { |i| "This is #{i} times"}
#=>  #<Proc:0x00007ffbc9c29a30@(pry):174>
proc = Proc.new { |i| "This is #{i} times"}
#=> #<Proc:0x00007ffbc9bc1b60@(pry):176>

存起來的 proc 就可以拿來使用

[1,2,3].map(&proc)
# ["This is 1 times", "This is 2 times", "This is 3 times"]

block的簡潔寫法&:

&:,就是使用了Symbol的 #to_proc 方法

:to_s.to_proc
# => #<Proc:0x00007ffbc47282e8(&:to_s)>

以前不懂的.map(&:method),原理就是 & 自動呼叫了 Symble 的 # to_proc方法,讓 symbol 被轉換成 proc 再代入

[1,2,3].map(&:to_s)
# ["1", "2", "3"]

# 效果等同:
[1,2,3].map {|i| i.to_s }

to_proc 原始碼大概長這樣

class Symbol
  def to_proc
    Proc.new { |i| i.send(self) }
  end
end

proc 直接作為參數

proc 直接作為參數的一部分,並用.call呼叫並輸入參數

def calculation(a, b, operation)
  operation.call(a, b)
end

puts calculation(5, 6, proc { |a, b| a + b }) 
# 11

lambda

第一次學Ruby,我連這個字都不會念。kk音標是[ˋlæmdə],其實就是物理學的波長λ。光速公式:c(光速)=λ(波長)×ν(頻率)
lambda 跟 proc 幾乎相等,但是 lambda 更像是 ruby 的 方法。
lambda 的兩種寫法:

lambda = lambda { |a, b| a + b } 
# => #<Proc:0x00007ffbc2e4dc28@(pry):208 (lambda)>
# 等同:
lambda = ->(a,b) { a + b } # 參數放外面
lambda.class
# => Proc    # 跟 proc 一樣是 Proc 的實體

proc vs lambda

proc 不檢查參數數目,沒帶入的參數會用nil取代,lambda 參數數目必須符合

proc = proc { |a, b| puts "first param: #{a},second param: #{b}" } 
proc.call 1
# first param: 1,second param: 

lambda = lambda { |a, b| puts "first param: #{a},second param: #{b}" } 
lambda.call 1
# ArgumentError: wrong number of arguments (given 1, expected 2)

當 lambda 內執行到 return ,控制權還是在呼叫它的方法,而繼續執行該方法後的片段;但當 Proc 內使用到 return 時,不會回到呼叫它的方法,而是立即跳出該方法:

def proc_return
  p 'first line'
  proc = Proc.new { return 10}
  "return value #{proc.call}"
end
 
proc_return
# "first line"
#=> 10

def lambda_return
  p 'first line'
  lambda = -> { return 10}
  "return value #{lambda.call}"
end
 
lambda_return
# "first line"
# => "return value 10"

所以說 lambda 更像ruby 方法,執行完繼續往下執行而不是跳出該方法


參考資料

  1. https://blog.appsignal.com/2018/09/04/ruby-magic-closures-in-ruby-blocks-procs-and-lambdas.html
  2. https://medium.com/@jinghua.shih/ruby-如何理解-ruby-block-2387b74f188b
  3. http://rubymonk.com/learning/books/4-ruby-primer-ascent/chapters/18-blocks/lessons/54-yield#solution3955
  4. https://pjchender.github.io/2017/09/26/ruby-程式碼區塊(block)-proc-和-lambda/

上一篇
[Day 1] 續說 Ruby on Rails
下一篇
[Day 3] Ruby Memoization
系列文
續說 Ruby on Rails10
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言