Ruby blocks, procs และ lambdas คืออะไร? มันทำงานอย่างไร? ต่างกันอย่างไร? อ่านบทความนี้จะรู้ทั้งนี้และมากกว่านั้น!
สารบัญ
- 1 ทำความเข้าใจ Ruby Blocks
- 2 Ruby Yield Keyword
- 3 Implicit vs Explicit Blocks
- 4 วิธีเช็กว่ามี Block ส่งมาหรือไม่
- 5 Lambda คืออะไร
- 6 Lambdas vs Procs
- 7 Closures
- 8 The Binding Class
- 9 Video Tutorial
- 10 สรุป
- 10.1 Related
ทำความเข้าใจ Ruby Blocks
Ruby blocks คือ anonymous functions เล็กๆ ที่ส่งเข้าเมธอดได้ Blocks ถูกล้อมรอบด้วย do / end หรือวงเล็บ {} และมี arguments หลายตัวได้ ชื่อ arguments อยู่ระหว่าง pipe | สองตัว
ถ้าเคยใช้ each ก็คือเคยใช้ blocks!
ตัวอย่าง:
# Form 1: แนะนำสำหรับ single line blocks
[1, 2, 3].each { |num| puts num }
^^^^^ ^^^^^^^^ block
block arguments body
# Form 2: แนะนำสำหรับ multi-line blocks [1, 2, 3].each do |num| puts num end
Ruby block มีประโยชน์เพราะเก็บ logic (code) ไว้ใช้ภายหลัง อาจเป็นการเขียนข้อมูลลงไฟล์, เปรียบเทียบ element, หรือ print error message
Ruby Yield Keyword
yield ใน Ruby หมายถึงอะไร? Yield เป็น Ruby keyword ที่เรียก block ตอนใช้งาน มันคือวิธีที่เมธอดใช้ blocks!
ตอนใช้ yield โค้ดใน block จะรัน ทำงานเหมือนตอนเรียกเมธอดทั่วไป
ตัวอย่าง:
def print_once
yield
end
print_once { puts "Block is being run" }
รัน block ที่ส่งให้ print_once ผลคือ "Block is being run" ปรินต์บนหน้าจอ
รู้ไหม... yield ใช้หลายครั้งได้! ทุกครั้งที่เรียก yield block จะรันใหม่ เหมือนเรียกเมธอดเดิมซ้ำ
ตัวอย่าง:
def print_twice
yield
yield
end
print_twice { puts "Hello" }
# "Hello"
# "Hello"
เหมือนเมธอด ส่ง arguments ให้ yield ได้ไม่จำกัดจำนวน
ตัวอย่าง:
def one_two_three
yield 1
yield 2
yield 3
end
one_two_three { |number| puts number * 10 }
# 10, 20, 30
Arguments นี้กลายเป็น block arguments ในตัวอย่างคือ number
Implicit vs Explicit Blocks
Blocks แบ่งเป็น "explicit" หรือ "implicit" Explicit คือให้ชื่อใน parameter list ส่ง explicit block ให้เมธอดอื่น หรือเก็บใน variable ใช้ภายหลังได้
ตัวอย่าง:
def explicit_block(&block)
block.call # same as yield
end
explicit_block { puts "Explicit block called" }
สังเกต &block parameter... นั่นคือวิธีตั้งชื่อ block!
วิธีเช็กว่ามี Block ส่งมาหรือไม่
ถ้า yield โดยไม่มี block จะได้ error no block given (yield) ใช้ block_given? method เช็กได้
ตัวอย่าง:
def do_something_with_block return "No block given" unless block_given? yield end
ป้องกัน error ถ้าใครเรียกเมธอดโดยไม่ส่ง block
Lambda คืออะไร
Lambda คือวิธี define block และ parameters ด้วย syntax พิเศษ เก็บ lambda ไว้ใน variable ใช้ภายหลังได้
Syntax สำหรับ define Ruby lambda:
say_something = -> { puts "This is a lambda" }
ใช้ syntax ทางเลือก:
lambdaแทน->ได้
Define lambda ไม่รันโค้ดภายใน เหมือน define method ไม่รันเมธอด ต้องใช้ call method
ตัวอย่าง:
say_something = -> { puts "This is a lambda" }
say_something.call # "This is a lambda"
มีวิธี call lambda อื่นๆ อีก รู้จักไว้ดี แต่แนะนำใช้ call เพื่อความชัดเจน
รายการ:
my_lambda = -> { puts "Lambda called" }
my_lambda.call
my_lambda.()
my_lambda[]
my_lambda.===
Lambdas รับ arguments ได้:
times_two = ->(x) { x * 2 }
times_two.call(10) # 20
ถ้าส่ง arguments ผิดจำนวนให้ lambda จะ raise exception เหมือนเมธอดทั่วไป
Lambdas vs Procs
Procs คล้ายมาก... ความต่างหนึ่งคือวิธีสร้าง
ตัวอย่าง:
my_proc = Proc.new { |x| puts x }
ไม่มี Lambda class แยก lambda คือ Proc object พิเศษ ดู instance methods ของ Proc จะเจอ lambda? method
ตอนนี้: Proc พฤติกรรมต่าง lambda โดยเฉพาะเรื่อง arguments:
t = Proc.new { |x,y| puts "I don't care about arguments!" }
t.call # "I don't care about arguments!"
ความต่างอีกอย่าง: reaction ต่อ return statement lambda จะ return ปกติเหมือนเมธอด แต่ proc พยายาม return จาก current context
หมายถึง: รันโค้ดนี้ จะเห็น proc raise LocalJumpError เพราะไม่สามารถ return จาก top-level context ได้
ลองดู:
# Should work
my_lambda = -> { return 1 }
puts "Lambda result: #{my_lambda.call}"
# Should raise exception
my_proc = Proc.new { return 1 }
puts "Proc result: #{my_proc.call}"
ถ้า proc อยู่ในเมธอด return จะเทียบเท่า return จากเมธอดนั้น แสดงในตัวอย่าง:
def call_proc
puts "Before proc"
my_proc = Proc.new { return 2 }
my_proc.call
puts "After proc"
end
p call_proc # Prints "Before proc" แต่ไม่พิมพ์ "After proc"
สรุปความต่าง procs และ lambdas:
- Lambdas define ด้วย
-> {}procs ด้วยProc.new {} - Procs return จาก current method lambdas return จาก lambda เอง
- Procs ไม่สนใจจำนวน arguments ถูกต้อง lambdas จะ raise exception
จากรายการนี้เห็นว่า lambdas ใกล้ regular method กว่า procs มาก
Closures
Ruby procs และ lambdas มี attribute พิเศษอีก: ตอนสร้าง Ruby proc จะ capture current execution scope ไปด้วย
Concept นี้เรียกว่า closure หมายถึง proc จะพกค่าตัวแปร local และเมธอดจาก context ที่ define ไปด้วย ไม่ได้พกค่าจริง แต่เป็น reference ถ้าตัวแปรเปลี่ยนหลังสร้าง proc proc จะได้ latest version เสมอ
ตัวอย่าง:
def call_proc(my_proc)
count = 500
my_proc.call
end
count = 1
my_proc = Proc.new { puts count }
p call_proc(my_proc) # พิมพ์อะไร?
มี local count = 1 มี proc my_proc และ call_proc method รัน proc ที่ส่งเข้าไป คิดว่าจะพิมพ์อะไร? ดูเหมือน 500 แต่งาน closure effect จะพิมพ์ 1 เพราะ proc ใช้ค่า count จากที่ define proc นั่นคือข้างนอก method definition
The Binding Class
Ruby procs และ lambdas เก็บ scope info ไว้ที่ไหน? มารู้จัก Binding class...
ตอนสร้าง Binding object ผ่าน binding method คุณสร้าง 'anchor' ที่จุดนี้ในโค้ด ทุก variable, method, class ที่ define ณ จุดนี้ จะเข้าถึงได้ผ่าน object นี้ แม้จะอยู่ scope ต่างหากก็ตาม
ตัวอย่าง:
def return_binding
foo = 100
binding
end
# Foo เข้าถึงได้ thanks to binding
# แม้เราอยู่นอกเมธอดที่มันถูก define
puts return_binding.class
puts return_binding.eval('foo')
# ถ้าพยายามพิมพ์ foo โดยตรงจะ error
# เพราะ foo ไม่ได้ define นอกเมธอด
puts foo
คือการ execute something under context ของ binding object เหมือนโค้ดนั้นอยู่ที่เดียวกับที่ binding ถูก define (จำ metaphor 'anchor' ไว้) ไม่ต้องใช้ binding objects โดยตรง แต่รู้จักไว้ก็ดีนะ 🙂
Video Tutorial
สรุป
ในโพสต์นี้คุณเรียนรู้ blocks ทำงานอย่างไร ความต่าง procs และ lambdas และ closure effect ที่เกิดตอนสร้าง block
เรื่องที่ไม่ได้ครอบคลุม: curry method เมธอดนี้ส่งบางส่วนหรือทั้งหมดของ required arguments ถ้าส่งบางส่วน จะได้ proc ใหม่ที่ arguments ถูก 'pre-loaded' แล้ว ตอนครบ arguments ทุกตัว proc จะ execute
หวังว่าจะชอบโพสต์นี้! อย่าลืมสมัครในฟอร์มด้านล่างและ แชร์ให้เพื่อนๆ 🙂