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

RBS: ระบบบันทึกชนิดข้อมูลใหม่ของ Ruby คำอธิบายครบถ้วน

RBS เป็นชื่อของภาษารูปแบบไวยากรณ์ชนิดข้อมูลใหม่สำหรับ Ruby RBS ช่วยให้คุณเพิ่มบันทึกชนิดข้อมูล (type annotations) ลงในโค้ด Ruby ของคุณในไฟล์ที่มีนามสกุลใหม่คือ .rbs โค้ดจะมีลักษณะดังนี้:

class MyClass def my_method : (my_param: String) -> String end 

ด้วยการให้บันทึกชนิดข้อมูลผ่าน RBS คุณจะได้รับประโยชน์ดังนี้:

  • วิธีที่สะอาดและกระชับในการกำหนดโครงสร้างของโค้ดเบส
  • วิธีที่ปลอดภัยกว่าในการเพิ่มชนิดข้อมูลลงในโค้ดเก่า (legacy code) ผ่านไฟล์แทนการเปลี่ยนแปลงคลาสโดยตรง
  • ศักยภาพในการรวมกับเครื่องมือตรวจสอบชนิดข้อมูลแบบคงที่ (static) และแบบไดนามิก (dynamic) ได้อย่างกว้างขวาง
  • ฟีเจอร์ใหม่ๆ สำหรับจัดการกับการโอเวอร์โหลดเมธอด (method overloading), ดักทายปิง (duck typing), อินเทอร์เฟซแบบไดนามิก และอื่นๆ

แต่ก่อน! ไม่มีเครื่องมือตรวจสอบชนิดข้อมูลแบบคงที่อย่าง Sorbet และ Steep อยู่แล้วหรือ?

มีจริง และเยอดเยี่ยมมาก! อย่างไรก็ตาม หลังจากการอภิปรายเป็นเวลา 4 ปี และเครื่องมือตรวจสอบชนิดข้อมูลที่สร้างโดยชุมชนหลายตัว ทีมคอมมิทเตอร์ของ Ruby คิดว่าถึงเวลาแล้วที่จะกำหนดมาตรฐานบางอย่างสำหรับการสร้างเครื่องมือตรวจสอบชนิดข้อมูล

RBS เป็นภาษาอย่างเป็นทางการ และกำลังจะมาพร้อมกับ Ruby 3

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

การติดตั้ง Ruby 3

เพื่อปฏิบัติตามตัวอย่างที่แสดงที่นี่ เราจำเป็นต้องติดตั้ง Ruby 3

คุณสามารถทำได้โดยปฏิบัติตามคำแนะนำอย่างเป็นทางการ หรือผ่าน ruby-build หากคุณต้องการจัดการหลายเวอร์ชันของ Ruby

ทางเลือกอื่นคือ คุณสามารถติดตั้ง gem rbs โดยตรงเข้าโปรเจกต์ปัจจุบันของคุณ:

gem install rbs 

การพิมพ์แบบคงที่ (Static) เทียบกับแบบไดนามิก (Dynamic)

ก่อนจะไปต่อ เรามาชี้แจงแนวคิดนี้กันก่อน ภาษาที่พิมพ์แบบไดนามิกเปรียบเทียบกับแบบคงที่อย่างไร?

ในภาษาที่พิมพ์แบบไดนามิก เช่น Ruby และ JavaScript ไม่มีชนิดข้อมูลที่กำหนดไว้ล่วงหน้าสำหรับอินเตอร์พรีเตอร์ให้เข้าใจวิธีดำเนินการเมื่อเกิดการดำเนินการที่ห้ามในขณะรันไทม์ (runtime)

นี่ตรงข้ามกับสิ่งที่คาดหวังจากการพิมพ์แบบคงที่ ภาษาที่พิมพ์แบบคงที่ เช่น Java และ C จะตรวจสอบชนิดข้อมูลในขณะคอมไพล์ไทม์ (compile time)

มาดูตัวอย่างโค้ด Java ด้านล่างเป็นข้อมูลอ้างอิง:

int number = 0; number = "Hi, number!"; 

สิ่งนี้เป็นไปไม่ได้ในการพิมพ์แบบคงที่ เนื่องจากบรรทัดที่สองจะโยนข้อผิดพลาด:

error: incompatible types: String cannot be converted to int 

ตอนนี้ มาลองตัวอย่างเดียวกันใน Ruby:

number = 0; number = "Hi, number!"; puts number // พิมพ์ "Hi, number!" สำเร็จ 

ใน Ruby ชนิดข้อมูลของตัวแปรจะเปลี่ยนไปได้ตามต้องการ ซึ่งหมายความว่าอินเตอร์พรีเตอร์รู้วิธีสลับจากชนิดหนึ่งไปอีกชนิดหนึ่งแบบไดนามิก

แนวคิดนี้มักสับสนกับภาษาที่ พิมพ์แบบแข็งแกร่ง (strongly typed) เทียบกับ พิมพ์แบบอ่อนแอ (weakly typed)

Ruby ไม่ได้เป็นภาษาที่พิมพ์แบบไดนามิกเพียงอย่างเดียว แต่ยังเป็นภาษาที่พิมพ์แบบแข็งแกร่งอีกด้วย ซึ่งหมายความว่ามันอนุญาตให้ตัวแปรเปลี่ยนชนิดข้อมูลระหว่างรันไทม์ได้ แต่ไม่อนุญาตให้คุณทำการผสมผสานชนิดข้อมูลที่ "บ้าคลั่ง" ได้

มาดูตัวอย่างต่อไป (ใน Ruby) ที่ปรับจากตัวอย่างก่อนหน้า:

number = 2; sum = "2" + 2; puts sum 

คราวนี้เราพยายามบวกตัวเลขสองตัวที่อยู่ในชนิดข้อมูลต่างกัน (Integer และ String) Ruby จะโยนข้อผิดพลาดดังนี้:

main.rb:2:in `+': no implicit conversion of Integer into String (TypeError) from main.rb:2:in `<main>' 

กล่าวอีกนัยหนึ่ง Ruby กำลังบอกว่างานในการคำนวณที่ซับซ้อนซึ่งเกี่ยวข้องกับชนิดข้อมูลที่อาจแตกต่างกันนั้น เป็นหน้าที่ของคุณทั้งหมด

JavaScript ในทางกลับกัน ซึ่งเป็นภาษาที่พิมพ์แบบอ่อนแอ และ แบบไดนามิก อนุญาตให้โค้ดเดียวกันนี้ทำงานด้วยผลลัพธ์ที่ต่างออกไป:

> number = 2 sum = "2" + 2 console.log(sum) > 22 // มันต่อสตริงทั้งสองค่าเข้าด้วยกัน 

RBS แตกต่างจาก Sorbet อย่างไร?

ข้อแรก จากแนวทางที่แต่ละตัวใช้ในการบันทึกชนิดข้อมูลโค้ด ในขณะที่ Sorbet ทำงานโดยการเพิ่มบันทึกชนิดข้อมูลอย่างชัดเจนทั่วทั้งโค้ดของคุณ RBS เพียงต้องการการสร้างไฟล์ใหม่ที่มีนามสกุล .rbs

ข้อได้เปรียบหลักของสิ่งนี้คือเมื่อเราคิดถึงการย้ายโค้ดเบสเก่า (legacy codebases) เนื่องจากไฟล์ต้นฉบับของคุณจะไม่ได้รับผลกระทบ จึงปลอดภัยกว่ามากในการนำไฟล์ RBS ไปใช้ในโปรเจกต์ของคุณ

ตามผู้สร้าง เป้าหมายหลักของ RBS คือ อธิบายโครงสร้าง ของโปรแกรม Ruby ของคุณ มันเน้นที่การกำหนดลายเซ็นของคลาส/เมธอดเท่านั้น

RBS เองไม่สามารถตรวจสอบชนิดข้อมูลได้ เป้าหมายของมันจำกัดอยู่ที่การกำหนดโครงสร้างเป็นพื้นฐานสำหรับเครื่องมือตรวจสอบชนิดข้อมูล (อย่าง Sorbet และ Steep) ทำหน้าที่ของพวกเขา

มาดูตัวอย่างการสืบทอด (inheritance) แบบง่ายๆ ใน Ruby:

class Badger def initialize(brand) @brand = brand end def brand? @brand end end class Honey < Badger def initialize(brand: "Honeybadger", sweet: true) super(brand) @sweet = sweet end def sweet? @sweet end end 

ยอดเยี่ยม! เพียงสองคลาสที่มีแอตทริบิวต์และชนิดข้อมูลที่อนุมานได้

ด้านล่างคือการแสดงผล RBS ที่เป็นไปได้:

class Brand attr_reader brand : String def initialize : (brand: String) -> void end class Honey < Brand @sweet : bool def initialize : (brand: String, ?sweet: bool) -> void def sweet? : () -> bool end 

ค่อนข้างคล้ายกันใช่ไหม? ความแตกต่างหลักในที่นี้คือชนิดข้อมูล เมธอด initialize ของคลาส Honey ตัวอย่างเช่น รับพารามิเตอร์หนึ่งเป็น String และหนึ่งเป็น boolean และไม่คืนค่าอะไร (void)

ในขณะเดียวกัน ทีม Sorbet กำลังทำงานใกล้ชิดกันในการสร้างเครื่องมือเพื่อให้เกิดความสามารถในการทำงานร่วมกันระหว่าง RBI (นามสกุลเริ่มต้นของ Sorbet สำหรับการกำหนดชนิดข้อมูล) และ RBS

เป้าหมายคือการวางพื้นฐานสำหรับ Sorbet และเครื่องมือตรวจสอบชนิดข้อมูลตัวอื่นๆ ให้เข้าใจวิธีใช้ไฟล์定义ชนิดข้อมูลของ RBS

เครื่องมือสร้างโครงร่าง (Scaffolding Tool)

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

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

เพื่อแสดงคำสั่งที่มีอยู่ ให้พิมพ์ rbs help ในคอนโซลและตรวจสอบผลลัพธ์:

RBS: ระบบบันทึกชนิดข้อมูลใหม่ของ Ruby คำอธิบายครบถ้วน คำสั่งที่มีอยู่ในเครื่องมือ CLI

คำสั่งที่สำคัญที่สุดในรายการน่าจะเป็น prototype เนื่องจากมันวิเคราะห์ ASTs ของไฟล์โค้ดต้นทางที่ให้มาเป็นพารามิเตอร์เพื่อสร้างโค้ด RBS "ประมาณการ" (approximate)

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

มาลองตัวอย่างอีกอันหนึ่งเป็นข้อมูลอ้างอิง คราวนี้เกี่ยวข้องกับสามคลาสที่แตกต่างกันในการสืบทอดแบบคาสเคด (cascaded inheritance):

class Animal def initialize(weight) @weight = weight end def breathe puts "Inhale/Exhale" end end class Mammal < Animal def initialize(weight, is_terrestrial) super(weight) @is_terrestrial = is_terrestrial end def nurse puts "I'm breastfeeding" end end class Cat < Mammal def initialize(weight, n_of_lives, is_terrestrial: true) super(weight, is_terrestrial) @n_of_lives = n_of_lives end def speak puts "Meow" end end 

แค่คลาสง่ายๆ ที่มีแอตทริบิวต์และเมธอด โปรดสังเกตว่ามีหนึ่งในนั้นที่ให้ค่า boolean เริ่มต้น ซึ่งจะสำคัญในการสาธิตว่ RBS สามารถเดาได้ด้วยตัวเองอย่างไร

ตอนนี้ เพื่อสร้างโครงร่างชนิดข้อมูลเหล่านี้ ให้รันคำสั่งต่อไปนี้:

rbs prototype rb animal.rb mammal.rb cat.rb 

คุณสามารถส่งไฟล์ Ruby หลายไฟล์ได้ตามต้องการ ผลลัพธ์ของการรันคำสั่งนี้คือ:

class Animal def initialize: (untyped weight) -> untyped def breathe: () -> untyped end class Mammal < Animal def initialize: (untyped weight, untyped is_terrestrial) -> untyped def nurse: () -> untyped end class Cat < Mammal def initialize: (untyped weight, untyped n_of_lives, ?is_terrestrial: bool is_terrestrial) -> untyped def speak: () -> untyped end 

ตามที่เราคาดการณ์ไว้ RBS ไม่สามารถเข้าใจชนิดข้อมูลส่วนใหญ่ที่เรามุ่งมั่นเมื่อสร้างคลาสได้

งานส่วนใหญ่ของคุณจะเป็นการเปลี่ยนอ้างอิง untyped ให้เป็นชนิดข้อมูลจริงด้วยตนเอง มีการอภิปรายบางอย่างที่มุ่งหาวิธีที่ดีกว่าในการทำสิ่งนี้กำลังเกิดขึ้นในชุมชนอยู่

เมตาโปรแกรมมิ่ง (Metaprogramming)

เมื่อพูดถึงเมตาโปรแกรมมิ่ง เครื่องมือ rbs จะไม่ช่วยได้มากนักเนื่องจากลักษณะไดนามิกของมัน

มาดูคลาสต่อไปนี้เป็นตัวอย่าง:

class Meta define_method :greeting, -> { puts 'Hi there!' } end Meta.new.greeting 

ผลลัพธ์ของการสร้างโครงร่างชนิดข้อมูลนี้จะเป็น:

class Meta end 

ดักทายปิง (Duck Typing)

Ruby ไม่ได้กังวลมากนักเกี่ยวกับลักษณะของออบเจ็กต์ (ชนิดข้อมูลของพวกมัน) แต่กังวลเกี่ยวกับสิ่งที่พวกมันทำได้ (สิ่งที่พวกมันทำ)

Duck typing เป็นสไตล์การเขียนโปรแกรมที่มีชื่อเสียงซึ่งทำงานตามคติ:

"ถ้าออบเจ็กต์พฤติกรรมเหมือนเป็ด (พูด, เดิน, บิน, ฯลฯ) แล้วมันก็คือเป็ด"

กล่าวอีกนัยหนึ่ง Ruby จะจัดการมันเสมอเหมือนเป็ด แม้ว่าการกำหนดและชนิดข้อมูลต้นฉบับของมันไม่ได้ควรจะแทนเป็ดก็ตาม

อย่างไรก็ตาม Duck typing อาจซ่อนรายละเอียดของการนำโค้ดไปใช้งานที่สามารถกลายเป็นเรื่องยุ่งยากและอ่าน/ค้นหาไม่ง่ายได้

RBS ได้นำเสนอแนวคิดของ ชนิดอินเทอร์เฟซ (interface types) ซึ่งเป็นชุดของเมธอดที่ไม่ขึ้นอยู่กับคลาสหรือโมดูลที่เป็นรูปธรรมใดๆ

มาลองนำตัวอย่างการสืบทอดสัตว์ก่อนหน้านี้มาใช้ และสมมติว่าเรากำลังเพิ่มระดับลำดับชั้นใหม่สำหรับสัตว์บก ซึ่ง Cat ของเราจะสืบทอดมาจากนั้น:

class Terrestrial < Animal def initialize(weight) super(weight) end def run puts "Running..." end end 

เพื่อป้องกันไม่ให้ออบเจ็กต์ลูกที่ไม่ใช่สัตว์บกวิ่งได้ เราสามารถสร้างอินเทอร์เฟซที่ตรวจสอบชนิดเฉพาะสำหรับการกระทำดังกล่าว:

interface _CanRun # ต้องการออปอเรเตอร์ `<<` ซึ่งยอมรับออบเจ็กต์ `Terrestrial` def <<: (Terrestrial) -> void end 

เมื่อแมปโค้ด RBS ไปยังเมธอด run เฉพาะเจาะจง ลายเซ็นจะเป็น:

def run: (_CanRun) -> void 

ทุกครั้งที่ใครพยายามส่งผ่านสิ่งอื่นนอกจากออบเจ็กต์ Terrestrial ให้กับเมธอด เครื่องมือตรวจสอบชนิดข้อมูลจะแน่ใจว่าบันทึกข้อผิดพลาด

ยูเนียนชนิด (Union Types)

นอกจากนี้ ในหมู่ Rubyists ยังพบได้บ่อยที่จะมีนิพจน์ที่เก็บค่าชนิดข้อมูลต่างๆ

def fly: () -> (Mammal | Bird | Insect) 

RBS รองรับยูเนียนชนิดโดยเพียงแค่เชื่อมต่อพวกมันผ่านออปอเรเตอร์ ไพพ์ (pipe)

การโอเวอร์โหลดเมธอด (Method Overloading)

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

มาลองตัวอย่างที่สัตว์สามารถคืนค่าพี่น้องวิวัฒนาการที่ใกล้ที่สุดได้:

def evolutionary_cousins: () -> Enumerator[Animal, void] | { (Animal) -> void } -> void 

ด้วยวิธีนี้ RBS ช่วยให้เราสามารถกำหนดได้อย่างชัดเจนว่าสัตว์ตัวใดตัวหนึ่งจะมีพี่น้องวิวัฒนาการเพียงตัวเดียวหรือหลายตัว

TypeProf

พร้อมกัน ทีม Ruby ยังได้เริ่มโปรเจกต์ใหม่ชื่อ typeprof ซึ่งเป็นอินเตอร์พรีเตอร์ระดับชนิดข้อมูลของ Ruby แบบทดลอง ที่มุ่งมั่นวิเคราะห์และ (พยายาม) สร้างเนื้อหา RBS

มันทำงานโดยการตีความเชิงนามธรรม (abstract interpretation) และยังอยู่ในขั้นตอนแรกๆ ของการปรับปรุงให้ดีขึ้น จึงควรระวังเมื่อใช้เพื่อวัตถุประสงค์การผลิต (production)

เพื่อติดตั้ง ให้เพียงแค่เพิ่ม gem เข้าโปรเจกต์ของคุณ:

gem install typeprof 

โปรดทราบว่ามันต้องการเวอร์ชัน Ruby สูงกว่า 2.7

มาดูเวอร์ชันต่อไปนี้ของคลาส Animal:

class Animal def initialize(weight) @weight = weight end def die(age) if age > 50 true elsif age <= 50 false elsif age < 0 nil end end end Animal.new(100).die(65) 

จากสิ่งที่เกิดขึ้นภายในเมธอด age และการเรียกเมธอดเดียวกันต่อไป TypeProf สามารถอนุมานชนิดข้อมูลที่ถูกจัดการในโค้ดได้อย่างฉลาด

เมื่อคุณรันคำสั่ง typeprof animal.rb ผลลัพธ์ควรจะเป็นดังนี้:

## Classes class Animal @weight: Integer def initialize: (Integer weight) -> Integer def die: (Integer age) -> bool? end 

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

การรวมกับ VS Code

ปัจจุบัน ไม่มีปลั๊กอิน VS Code มากนักสำหรับจัดการกับ RBS สำหรับการจัดรูปแบบ ตรวจสอบโครงสร้าง ฯลฯ โดยเฉพาะเพราะมันยังค่อนข้างใหม่

อย่างไรก็ตาม หากคุณค้นหาในสโตร์ด้วยคำว่า "RBS" คุณอาจพบปลั๊กอินหนึ่งตัวชื่อ ruby-signature ซึ่งจะช่วยในการไฮไลท์ไวยากรณ์ (syntax highlighting) ตามที่แสดงด้านล่าง:

RBS: ระบบบันทึกชนิดข้อมูลใหม่ของ Ruby คำอธิบายครบถ้วน การไฮไลท์ไวยากรณ์ RBS ใน VS Code

สรุป

RBS เพิ่งเพ็ทใหม่และแล้วแต่ก็เป็นขั้นตอนสำคัญสู่โค้ดเบส Ruby ที่ปลอดภัยขึ้น

ปกติแล้ว ในเวลาอันจะมาถึง เครื่องมือใหม่และโปรเจกต์โอเพนซอร์สจะขึ้นมารองรับมัน เช่น RBS Rails สำหรับสร้างไฟล์ RBS สำหรับแอปพลิเคชัน Ruby on Rails

อนาคตมีสิ่งยอดเยี่ยมรออยู่ข้างหน้าสำหรับชุมชน Ruby ด้วยแอปพลิเคชันที่ปลอดภัยและปราศจากบั๊กมากขึ้น รอไม่及แล้ว!