Ruby Metaprogramming นั้นเจ๋งกว่าเสียง
เผยแพร่แล้ว: 2022-03-11คุณมักจะได้ยินว่า metaprogramming เป็นสิ่งที่นินจา Ruby เท่านั้นใช้ และไม่เหมาะสำหรับปุถุชนทั่วไป แต่ความจริงก็คือว่า metaprogramming ไม่ได้เป็นสิ่งที่น่ากลัวเลย โพสต์บล็อกนี้จะใช้เพื่อท้าทายความคิดประเภทนี้และเพื่อให้โปรแกรมเมตาโปรแกรมเมอร์ใกล้ชิดกับนักพัฒนา Ruby โดยเฉลี่ยมากขึ้น เพื่อให้สามารถเก็บเกี่ยวผลประโยชน์ได้
ควรสังเกตว่า metaprogramming อาจมีความหมายมากและมักถูกใช้ในทางที่ผิดและกลายเป็นเรื่องสุดขั้วเมื่อพูดถึงการใช้งาน ดังนั้นฉันจะพยายามยกตัวอย่างในโลกแห่งความเป็นจริงที่ทุกคนสามารถใช้ในการเขียนโปรแกรมในชีวิตประจำวันได้
Metaprogramming
Metaprogramming เป็นเทคนิคที่คุณสามารถเขียนโค้ดที่ เขียน โค้ดได้เองแบบไดนามิกที่รันไทม์ ซึ่งหมายความว่าคุณสามารถกำหนดวิธีการและคลาสระหว่างรันไทม์ได้ บ้าใช่มั้ย? โดยสรุป การใช้โปรแกรมเมตาโปรแกรมสามารถเปิดใหม่และแก้ไขคลาส ตรวจจับเมธอดที่ไม่มีอยู่ และสร้างได้ทันที สร้างโค้ดที่แห้งโดยหลีกเลี่ยงการซ้ำซ้อน และอื่นๆ
พื้นฐาน
ก่อนที่เราจะดำดิ่งสู่ metaprogramming อย่างจริงจัง เราต้องสำรวจพื้นฐานก่อน และวิธีที่ดีที่สุดคือการยกตัวอย่าง มาเริ่มกันและทำความเข้าใจ Ruby metaprogramming ทีละขั้นตอน คุณอาจเดาได้ว่าโค้ดนี้กำลังทำอะไรอยู่:
class Developer def self.backend "I am backend developer" end def frontend "I am frontend developer" end end
เราได้กำหนดคลาสด้วยสองวิธี เมธอดแรกในคลาสนี้คือเมธอดของคลาส และเมธอดที่สองคือเมธอดอินสแตนซ์ นี่เป็นสิ่งพื้นฐานใน Ruby แต่มีอีกมากมายเกิดขึ้นเบื้องหลังโค้ดนี้ ซึ่งเราต้องเข้าใจก่อนดำเนินการต่อไป เป็นเรื่องที่ควรค่าแก่การชี้ให้เห็นว่าคลาส Developer
นั้นแท้จริงแล้วเป็นวัตถุ ใน Ruby ทุกอย่างเป็นวัตถุรวมถึงคลาส เนื่องจาก Developer
เป็นตัวอย่าง จึงเป็นอินสแตนซ์ของ class Class
นี่คือลักษณะของโมเดลวัตถุ Ruby:
p Developer.class # Class p Class.superclass # Module p Module.superclass # Object p Object.superclass # BasicObject
สิ่งสำคัญอย่างหนึ่งที่ต้องเข้าใจในที่นี้คือความหมายของ self
เมธอด frontend
เป็นวิธีปกติที่มีอยู่ในอินสแตนซ์ของ class Developer
แต่ทำไมเมธอด backend
จึงเป็นเมธอดของคลาส โค้ดทุกชิ้นที่ดำเนินการใน Ruby จะถูกดำเนินการกับ ตนเอง โดยเฉพาะ เมื่อล่าม Ruby รันโค้ดใด ๆ มันจะติดตามค่าของ self
สำหรับบรรทัดใดก็ตาม self
หมายถึงวัตถุบางอย่างเสมอ แต่วัตถุนั้นสามารถเปลี่ยนแปลงได้ตามรหัสที่ดำเนินการ ตัวอย่างเช่น ภายในนิยามคลาส self
อ้างถึงคลาสซึ่งเป็นตัวอย่างของคลาส Class
class Developer p self end # Developer
ภายในเมธอดของอินสแตนซ์ self
หมายถึงอินสแตนซ์ของคลาส
class Developer def frontend self end end p Developer.new.frontend # #<Developer:0x2c8a148>
ภายในวิธีการเรียน self
หมายถึงตัวชั้นเรียนเอง (ซึ่งจะกล่าวถึงในรายละเอียดเพิ่มเติมในบทความนี้):
class Developer def self.backend self end end p Developer.backend # Developer
นี่เป็นเรื่องปกติ แต่วิธีการเรียนคืออะไร? ก่อนตอบคำถามนั้น เราต้องพูดถึงการมีอยู่ของสิ่งที่เรียกว่า metaclass หรือที่เรียกว่า singleton class และ eigenclass frontend
ของเมธอดคลาสที่เรากำหนดไว้ก่อนหน้านี้ไม่มีอะไรเลยนอกจากเมธอดอินสแตนซ์ที่กำหนดไว้ในเมตาคลาสสำหรับอ็อบเจกต์ Developer
! โดยพื้นฐานแล้ว metaclass เป็นคลาสที่ Ruby สร้างและแทรกลงในลำดับชั้นการสืบทอดเพื่อเก็บเมธอดของคลาส ดังนั้นจึงไม่รบกวนอินสแตนซ์ที่สร้างขึ้นจากคลาส
Metaclasses
ทุกอ็อบเจ็กต์ใน Ruby มี metaclass ของตัวเอง นักพัฒนาไม่สามารถมองเห็นได้ แต่อยู่ที่นั่นและคุณสามารถใช้งานได้ง่ายมาก เนื่องจากคลาส Developer
ของเราเป็นอ็อบเจ็กต์โดยพื้นฐานแล้ว มันมีเมตาคลาสของตัวเอง ตัวอย่างเช่น เรามาสร้างวัตถุของคลาส String
และจัดการ metaclass ของมัน:
example = "I'm a string object" def example.something self.upcase end p example.something # I'M A STRING OBJECT
สิ่งที่เราทำที่นี่คือเราได้เพิ่มเมธอดซิงเกิลตัน something
ลงในอ็อบเจ็กต์ ความแตกต่างระหว่างเมธอดของคลาสและเมธอดซิงเกิลตันคือเมธอดของคลาสนั้นใช้ได้กับอินสแตนซ์ทั้งหมดของอ็อบเจ็กต์คลาส ในขณะที่เมธอดซิงเกิลตันนั้นใช้ได้เฉพาะกับอินสแตนซ์เดียวเท่านั้น เมธอดของคลาสนั้นใช้กันอย่างแพร่หลายในขณะที่เมธอด singleton ไม่มากนัก แต่เมธอดทั้งสองประเภทจะถูกเพิ่มใน metaclass ของอ็อบเจกต์นั้น
ตัวอย่างก่อนหน้านี้สามารถเขียนใหม่ได้ดังนี้:
example = "I'm a string object" class << example def something self.upcase end end
ไวยากรณ์แตกต่างกัน แต่มีประสิทธิภาพในสิ่งเดียวกัน ตอนนี้ ให้กลับไปที่ตัวอย่างก่อนหน้านี้ที่เราสร้างคลาส Developer
และสำรวจไวยากรณ์อื่นๆ เพื่อกำหนดวิธีการของคลาส:
class Developer def self.backend "I am backend developer" end end
นี่เป็นคำจำกัดความพื้นฐานที่เกือบทุกคนใช้
def Developer.backend "I am backend developer" end
นี่คือสิ่งเดียวกัน เรากำลังกำหนดวิธีการ backend
คลาสสำหรับ Developer
เราไม่ได้ใช้ self
แต่การกำหนดวิธีการเช่นนี้ทำให้เป็นวิธีการเรียนได้อย่างมีประสิทธิภาพ
class Developer class << self def backend "I am backend developer" end end end
อีกครั้ง เรากำลังกำหนดเมธอดของคลาส แต่ใช้ไวยากรณ์ที่คล้ายกับวิธีที่เราใช้กำหนดเมธอดซิงเกิลตันสำหรับออบเจกต์ String
คุณอาจสังเกตเห็นว่าเราใช้ self
ที่นี้ ซึ่งหมายถึงวัตถุ Developer
เอง ขั้นแรก เราเปิดคลาส Developer
ทำให้ตัวเองเท่ากับ Developer
class ต่อไป เราทำ class << self
ทำให้ self เท่ากับ metaclass ของ Developer
จากนั้นเรากำหนดเมธอด backend
บน metaclass ของ Developer
class << Developer def backend "I am backend developer" end end
โดยการกำหนดบล็อกเช่นนี้ เรากำลังตั้งค่า self
เป็น metaclass ของ Developer
ตลอดระยะเวลาของบล็อก ด้วยเหตุนี้ เมธอด backend
จึงถูกเพิ่มใน metaclass ของ Developer
แทนที่จะเป็นคลาสเอง
มาดูกันว่า metaclass นี้ทำงานอย่างไรในแผนผังการสืบทอด:
ดังที่คุณเห็นในตัวอย่างก่อนหน้านี้ ไม่มีข้อพิสูจน์ที่แท้จริงว่า metaclass มีอยู่จริง แต่เราสามารถใช้แฮ็คเล็กๆ น้อยๆ ที่สามารถแสดงให้เราเห็นถึงการมีอยู่ของคลาสที่มองไม่เห็นนี้ได้:
class Object def metaclass_example class << self self end end end
หากเรากำหนดเมธอดอินสแตนซ์ในคลาส Object
(ใช่ เราสามารถเปิดคลาสใดๆ ใหม่ได้ทุกเมื่อ นั่นเป็นอีกความสวยงามของเมตาโปรแกรมมิ่ง) เราจะมี self
ที่อ้างอิงถึงออบเจกต์ Object
ภายในนั้น จากนั้นเราสามารถใช้ class << self
syntax เพื่อเปลี่ยน self ปัจจุบันให้ชี้ไปที่ metaclass ของวัตถุปัจจุบัน เนื่องจากอ็อบเจ็กต์ปัจจุบันเป็นคลาส Object
เอง นี่จึงเป็น metaclass ของอินสแตนซ์ เมธอดส่งคืน self
ซึ่ง ณ จุดนี้ metaclass เอง ดังนั้นการเรียกเมธอดอินสแตนซ์นี้บนอ็อบเจกต์ใดๆ เราก็สามารถรับ metaclass ของอ็อบเจ็กต์นั้นได้ มากำหนดคลาส Developer
ของเราอีกครั้งและเริ่มสำรวจกัน:

class Developer def frontend p "inside instance method, self is: " + self.to_s end class << self def backend p "inside class method, self is: " + self.to_s end end end developer = Developer.new developer.frontend # "inside instance method, self is: #<Developer:0x2ced3b8>" Developer.backend # "inside class method, self is: Developer" p "inside metaclass, self is: " + developer.metaclass_example.to_s # "inside metaclass, self is: #<Class:#<Developer:0x2ced3b8>>"
และสำหรับ crescent เรามาดูการพิสูจน์ว่า frontend
เป็นวิธีอินสแตนซ์ของคลาส และ backend
เป็นวิธีอินสแตนซ์ของ metaclass:
p developer.class.instance_methods false # [:frontend] p developer.class.metaclass_example.instance_methods false # [:backend]
แม้ว่าในการรับ metaclass คุณไม่จำเป็นต้องเปิด Object
ใหม่อีกครั้งและเพิ่มการแฮ็กนี้ คุณสามารถใช้ singleton_class
ที่ Ruby มีให้ มันเหมือนกับ metaclass_example
ที่เราเพิ่มเข้าไป แต่ด้วยแฮ็คนี้ คุณจะเห็นว่า Ruby ทำงานอย่างไรภายใต้ประทุน:
p developer.class.singleton_class.instance_methods false # [:backend]
การกำหนดวิธีการโดยใช้ “class_eval” และ “instance_eval”
มีอีกวิธีหนึ่งในการสร้างเมธอดของคลาส นั่นคือการใช้ instance_eval
:
class Developer end Developer.instance_eval do p "instance_eval - self is: " + self.to_s def backend p "inside a method self is: " + self.to_s end end # "instance_eval - self is: Developer" Developer.backend # "inside a method self is: Developer"
ตัวแปลรหัส Ruby ชิ้นนี้ประเมินในบริบทของอินสแตนซ์ ซึ่งในกรณีนี้คือวัตถุ Developer
และเมื่อคุณกำหนดเมธอดบนอ็อบเจ็กต์ คุณกำลังสร้างเมธอดของคลาสหรือเมธอดซิงเกิลตัน ในกรณีนี้คือเมธอดของคลาส - ถ้าจะพูดให้ถูกก็คือ เมธอดของคลาสคือเมธอดซิงเกิลตัน แต่เป็นเมธอดซิงเกิลตันของคลาส ขณะที่เมธอดแบบซิงเกิลตันของอ็อบเจ็กต์
ในทางกลับกัน class_eval
จะประเมินโค้ดในบริบทของคลาสแทนที่จะเป็นอินสแตนซ์ มันเกือบจะเปิดชั้นเรียนอีกครั้ง นี่คือวิธีการใช้ class_eval
เพื่อสร้างวิธีการอินสแตนซ์:
Developer.class_eval do p "class_eval - self is: " + self.to_s def frontend p "inside a method self is: " + self.to_s end end # "class_eval - self is: Developer" p developer = Developer.new # #<Developer:0x2c5d640> developer.frontend # "inside a method self is: #<Developer:0x2c5d640>"
โดยสรุป เมื่อคุณเรียกใช้เมธอด class_eval
คุณจะเปลี่ยน self
เพื่ออ้างถึงคลาสดั้งเดิม และเมื่อคุณเรียก instance_eval
การเปลี่ยนแปลง self
เพื่ออ้างถึงคลาส meta ดั้งเดิม
การกำหนดวิธีการที่ขาดหายไปในการบิน
ปริศนา metaprogramming อีกหนึ่งชิ้นคือ method_missing
เมื่อคุณเรียกใช้เมธอดบนอ็อบเจ็กต์ อันดับแรก Ruby จะเข้าสู่คลาสและเรียกดูเมธอดของอินสแตนซ์ หากไม่พบวิธีการที่นั่น มันก็จะค้นหาสายบรรพบุรุษต่อไป หาก Ruby ยังไม่พบเมธอด จะเรียกเมธอดอื่นชื่อ method_missing
ซึ่งเป็นเมธอดอินสแตนซ์ของ Kernel
ที่ทุกอ็อบเจกต์สืบทอดมา เนื่องจากเรามั่นใจว่า Ruby จะเรียกวิธีนี้ในที่สุดสำหรับวิธีที่หายไป เราจึงสามารถใช้วิธีนี้เพื่อใช้ลูกเล่นบางอย่างได้
define_method
เป็นวิธีการที่กำหนดไว้ในคลาส Module
ซึ่งคุณสามารถใช้เพื่อสร้างวิธีการแบบไดนามิก ในการใช้ define_method
คุณเรียกมันด้วยชื่อของเมธอดใหม่และบล็อกที่พารามิเตอร์ของบล็อกกลายเป็นพารามิเตอร์ของเมธอดใหม่ ความแตกต่างระหว่างการใช้ def
เพื่อสร้างเมธอดและ define_method
อย่างไร ไม่มีอะไรแตกต่างกันมาก ยกเว้นคุณสามารถใช้ define_method
ร่วมกับ method_missing
เพื่อเขียนโค้ด DRY พูดให้ถูกก็คือ คุณสามารถใช้ define_method
แทน def
เพื่อจัดการขอบเขตเมื่อกำหนดคลาส แต่นั่นเป็นอีกเรื่องหนึ่งทั้งหมด ลองมาดูตัวอย่างง่ายๆ:
class Developer define_method :frontend do |*my_arg| my_arg.inject(1, :*) end class << self def create_backend singleton_class.send(:define_method, "backend") do "Born from the ashes!" end end end end developer = Developer.new p developer.frontend(2, 5, 10) # => 100 p Developer.backend # undefined method 'backend' for Developer:Class (NoMethodError) Developer.create_backend p Developer.backend # "Born from the ashes!"
นี่แสดงให้เห็นว่า define_method
ถูกใช้อย่างไรเพื่อสร้างวิธีการอินสแตนซ์โดยไม่ต้องใช้ def
อย่างไรก็ตาม ยังมีอะไรอีกมากมายที่เราสามารถทำได้กับพวกเขา ลองดูที่ข้อมูลโค้ดนี้:
class Developer def coding_frontend p "writing frontend" end def coding_backend p "writing backend" end end developer = Developer.new developer.coding_frontend # "writing frontend" developer.coding_backend # "writing backend"
รหัสนี้ไม่ใช่ DRY แต่การใช้ define_method
เราสามารถทำให้เป็น DRY ได้:
class Developer ["frontend", "backend"].each do |method| define_method "coding_#{method}" do p "writing " + method.to_s end end end developer = Developer.new developer.coding_frontend # "writing frontend" developer.coding_backend # "writing backend"
ดีกว่ามาก แต่ก็ยังไม่สมบูรณ์แบบ ทำไม? หากเราต้องการเพิ่มวิธีการใหม่เช่น coding_debug
เราจำเป็นต้องใส่ "debug"
นี้ลงในอาร์เรย์ แต่การใช้ method_missing
เราสามารถแก้ไขได้:
class Developer def method_missing method, *args, &block return super method, *args, &block unless method.to_s =~ /^coding_\w+/ self.class.send(:define_method, method) do p "writing " + method.to_s.gsub(/^coding_/, '').to_s end self.send method, *args, &block end end developer = Developer.new developer.coding_frontend developer.coding_backend developer.coding_debug
โค้ดชิ้นนี้ซับซ้อนเล็กน้อย เรามาแยกย่อยกัน การเรียกเมธอดที่ไม่มีอยู่จะทำให้ method_missing
เริ่มทำงาน ที่นี่ เราต้องการสร้างวิธีการใหม่เฉพาะเมื่อชื่อวิธีการขึ้นต้นด้วย "coding_"
ไม่อย่างนั้นเราก็แค่เรียก super เพื่อทำหน้าที่รายงานวิธีการที่ขาดหายไปจริงๆ และเราเพียงแค่ใช้ define_method
เพื่อสร้างวิธีการใหม่นั้น แค่นั้นแหละ! ด้วยโค้ดชิ้นนี้ เราสามารถสร้างเมธอดใหม่นับพันรายการโดยเริ่มจาก "coding_"
และความจริงก็คือสิ่งที่ทำให้โค้ดของเราแห้ง เนื่องจาก define_method
เป็นส่วนตัวกับ Module
เราจึงต้องใช้ send
เพื่อเรียกใช้
ห่อ
นี่เป็นเพียงส่วนปลายของภูเขาน้ำแข็ง ในการเป็น Ruby Jedi นี่คือจุดเริ่มต้น หลังจากที่คุณเชี่ยวชาญหน่วยการสร้างของโปรแกรมเมตาโปรแกรมมิ่งเหล่านี้และเข้าใจสาระสำคัญของมันอย่างแท้จริงแล้ว คุณสามารถดำเนินการบางอย่างที่ซับซ้อนมากขึ้นได้ เช่น สร้างภาษาเฉพาะโดเมน (DSL) ของคุณเอง DSL เป็นหัวข้อในตัวเอง แต่แนวคิดพื้นฐานเหล่านี้เป็นข้อกำหนดเบื้องต้นในการทำความเข้าใจหัวข้อขั้นสูง Gem ที่ใช้มากที่สุดใน Rails บางส่วนถูกสร้างขึ้นในลักษณะนี้ และคุณอาจใช้ DSL โดยที่คุณไม่รู้ตัว เช่น RSpec และ ActiveRecord
หวังว่าบทความนี้จะช่วยให้คุณเข้าใจ metaprogramming มากขึ้นอีกขั้น และอาจถึงขั้นสร้าง DSL ของคุณเอง ซึ่งคุณสามารถใช้เขียนโค้ดได้อย่างมีประสิทธิภาพมากขึ้น