Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

เรียนรู้ Ruby Closures: เข้าใจ Blocks, Procs, และ Lambdas อย่างลึกซึ้ง

ใน Ruby Magic เราชอบดำดิ่งสู่ magic ด้านหลังสิ่งที่เราใช้ทุกวันเพื่อเข้าใจว่ามันทำงานอย่างไร ใน edition นี้ เราจะสำรวจความแตกต่างระหว่าง blocks, procs และ lambdas

ใน programming languages ที่มี first-class functions functions สามารถเก็บใน variables และส่งเป็น arguments ให้ functions อื่นๆ Functions ยังสามารถใช้ functions อื่นๆ เป็น return values ได้

Closure คือ first-class function พร้อม environment Environment คือ mapping ไปยัง variables ที่มีอยู่เมื่อ closure ถูกสร้าง Closure จะ retain access ไปยัง variables เหล่านี้ แม้จะถูกกำหนดใน scope อื่น

Ruby ไม่มี first-class functions แต่มี closures ในรูปของ blocks, procs และ lambdas Blocks ใช้สำหรับส่ง blocks ของ code ให้ methods และ procs กับ lambdas อนุญาตให้เก็บ blocks ของ code ใน variables

Blocks

ใน Ruby blocks คือ code snippets ที่สร้างขึ้นเพื่อ execute ภายหลัง Blocks ถูกส่งให้ methods ที่ yield พวกมันภายใน keywords do และ end ตัวอย่างหนึ่งคือ method #each ซึ่ง loop over enumerable objects

[1,2,3].each do |n| puts "#{n}!" end [1,2,3].each { |n| puts "#{n}!" } # the one-line equivalent.

ในตัวอย่างนี้ block ถูกส่งให้ Array#each method ซึ่งรัน block สำหรับแต่ละ item ใน array และ print ไปที่ console

def each i = 0 while i < size yield at(i) i += 1 end end

ในตัวอย่าง simplified ของ Array#each ใน while loop yield ถูกเรียกเพื่อ execute passed block สำหรับทุก item ใน array โปรดสังเกต method นี้ไม่มี arguments เพราะ block ถูกส่งให้ method implicitly

Implicit Blocks และ Keyword yield

ใน Ruby methods สามารถรับ blocks ได้ทั้ง implicitly และ explicitly Implicit block passing ทำงานโดยการเรียก keyword yield ใน method Keyword yield เป็นพิเศษ มันหาและเรียก passed block ดังนั้นคุณไม่ต้องเพิ่ม block เข้าไปใน list of arguments ที่ method รับ

เพราะ Ruby อนุญาต implicit block passing คุณสามารถเรียกทุก methods พร้อม block ถ้ามันไม่ call yield block จะถูก ignore

irb> "foo bar baz".split { p "block!" } => ["foo", "bar", "baz"]

ถ้า called method does yield passed block จะถูกหาและเรียกพร้อม arguments ที่ส่งไปกับ keyword yield

def each return to_enum(:each) unless block_given? i = 0 while i < size yield at(i) i += 1 end end

ตัวอย่างนี้คืน instance ของ Enumerator เว้นแต่ block จะถูกให้

Keywords yield และ block_given? หา block ใน current scope นี่ช่วยให้ส่ง blocks implicitly ได้ แต่ป้องกัน code ไม่ให้ access block โดยตรงเพราะมันไม่ได้เก็บใน variable

Explicitly Passing Blocks

เราสามารถ explicitly accept block ใน method โดยเพิ่มเป็น argument ด้วย ampersand parameter (มักเรียก &block) เนื่องจาก block ตอนนี้ explicit เราสามารถใช้ method #call โดยตรงบน resulting object แทนที่จะพึ่งพา yield

Argument &block ไม่ใช่ proper argument ดังนั้นการเรียก method นี้กับอะไรก็ตามนอกจาก block จะผลิต ArgumentError

def each_explicit(&block) return to_enum(:each) unless block i = 0 while i < size block.call at(i) i += 1 end end

เมื่อ block ถูกส่งแบบนี้และเก็บใน variable มันจะถูก convert เป็น proc อัตโนมัติ

Procs

"Proc" คือ instance ของ class Proc ซึ่งเก็บ code block เพื่อ execute และสามารถเก็บใน variable ได้ เพื่อสร้าง proc คุณเรียก Proc.new และส่ง block ให้

proc = Proc.new { |n| puts "#{n}!" }

เพราะ proc สามารถเก็บใน variable มันจึงสามารถส่งให้ method เหมือน normal argument ได้ ในกรณีนี้ เราไม่ใช้ ampersand เพราะ proc ถูกส่ง explicitly

def run_proc_with_random_number(proc) proc.call(random) end proc = Proc.new { |n| puts "#{n}!" } run_proc_with_random_number(proc)

แทนที่จะสร้าง proc และส่งให้ method คุณสามารถใช้ Ruby's ampersand parameter syntax ที่เห็นก่อนหน้านี้และใช้ block แทน

def run_proc_with_random_number(&proc) proc.call(random) end run_proc_with_random_number { |n| puts "#{n}!" }

โปรดสังเกต ampersand ที่เพิ่มเข้ามาใน argument ของ method นี้จะ convert passed block เป็น proc object และเก็บใน variable ใน method scope

Tip: แม้จะ useful ที่จะมี proc ใน method ในบาง situations การ convert block เป็น proc สร้าง performance hit เมื่อเป็นไปได้ ใช้ implicit blocks แทน

#to_proc

Symbols, hashes และ methods สามารถ convert เป็น procs ได้ด้วย #to_proc methods ของพวกมัน การใช้งานที่พบบ่อยคือการส่ง proc ที่สร้างจาก symbol ให้ method

[1,2,3].map(&:to_s) [1,2,3].map {|i| i.to_s } [1,2,3].map {|i| i.send(:to_s) }

ตัวอย่างนี้แสดง 3 วิธี equivalent ของการเรียก #to_s บนแต่ละ element ของ array ในวิธีแรก symbol ที่ prefix ด้วย ampersand ถูกส่ง ซึ่ง auto convert เป็น proc โดยเรียก #to_proc method ของมัน สองวิธีสุดท้ายแสดง proc นั้นอาจมีรูปแบบอย่างไร

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

แม้จะเป็น simplified example implementation ของ Symbol#to_proc แสดงสิ่งที่เกิดขึ้นใต้หลอดมือ Method คืน proc ที่รับ one argument และส่ง self ให้มัน เนื่องจาก self คือ symbol ใน context นี้ มันจึงเรียก method Integer#to_s

Lambdas

Lambdas คือ procs พร้อม distinguishing factors บางอย่าง พวกมันคล้าย "regular" methods มากกว่าใน 2 ทาง: พวกมัน enforce จำนวน arguments ที่ส่งเมื่อถูกเรียก และใช้ "normal" returns

เมื่อเรียก lambda ที่คาดหวัง argument โดยไม่ส่ง หรือส่ง argument ให้ lambda ที่ไม่คาดหวัง Ruby จะ raise ArgumentError

irb> lambda (a) { a }.call ArgumentError: wrong number of arguments (given 0, expected 1) from (irb):8:in `block in irb_binding' from (irb):8 from /Users/jeff/.asdf/installs/ruby/2.3.0/bin/irb:11:in `
'

นอกจากนี้ lambda จัดการกับ keyword return เหมือน method เมื่อเรียก proc โปรแกรม yield control ไปยัง code block ใน proc ดังนั้น ถ้า proc returns current scope จะ return ถ้า proc ถูกเรียกภายใน function และเรียก return function จะ return ทันทีเช่นกัน

def return_from_proc a = Proc.new { return 10 }.call puts "This will never be printed." end

Function นี้จะ yield control ให้ proc ดังนั้นเมื่อมัน return function ก็ return การเรียก function ในตัวอย่างนี้จะไม่ print output และคืน 10

def return_from_lambda a = lambda { return 10 }.call puts "The lambda returned #{a}, and this will be printed." end

เมื่อใช้ lambda มัน จะ print การเรียก return ใน lambda จะทำงานเหมือนเรียก return ใน method ดังนั้น variable a ถูก populate ด้วย 10 และบรรทัดถูก print ไปที่ console

Blocks, procs และ lambdas

ตอนนี้เราได้ดำดิ่งจนสุดทั้ง blocks, procs และ lambdas มา zoom ออกและสรุปการเปรียบเทียบกัน

  • Blocks ถูกใช้อย่างกว้างขวางใน Ruby สำหรับส่ง bits ของ code ให้ functions โดยใช้ keyword yield block สามารถส่ง implicitly ได้โดยไม่ต้อง convert เป็น proc
  • เมื่อใช้ parameters ที่ prefix ด้วย ampersands การส่ง block ให้ method จะผลิต proc ใน context ของ method Procs ทำงานเหมือน blocks แต่สามารถเก็บใน variable ได้
  • Lambdas คือ procs ที่ทำงานเหมือน methods หมายถึง enforce arity และ return เหมือน methods แทนที่จะเป็นใน parent scope

นี่คือการมุจฉาย closures ใน Ruby ใน Ruby Magic ตอนหน้า ยังมีอะไรให้เรียนรู้เกี่ยวกับ closures เช่น lexical scopes และ bindings แต่เราจะเก็บไว้ตอนหน้า ในขณะนี้ โปรดบอกเราว่าอยากอ่านอะไรใน future installment ของ Ruby Magic closures หรืออื่นๆ ที่ @AppSignal