ในที่สุด Ruby เวอร์ชัน 3.0.0 ที่รอคอยมานานก็ถูกปล่อยออกมาแล้ว นอกจากการปรับปรุงที่ยอดเยี่ยมมากมายแล้ว เช่น การเพิ่มประสิทธิภาพเร็วขึ้น 3 เท่า เมื่อเทียบกับเวอร์ชันก่อนหน้า คุณลักษณะการทดสอบพร้อมกันและขนานกัน เป็นต้น ทีม Ruby ยังได้แนะนำภาษาไวยากรณ์ใหม่สำหรับการพิมพ์แบบไดนามิกใน Ruby:RBS
นั่นคือสิ่งที่ทีมคุยกันมาหลายปีแล้ว โดยอิงจากความสำเร็จของเครื่องมือที่พัฒนาโดยชุมชนสำหรับการตรวจสอบประเภทคงที่ เช่น Sorbet
Sorbet เป็นตัวตรวจสอบประเภทที่ทรงพลังซึ่งสนับสนุนโดย Stripe โดยจะตรวจสอบโค้ดของคุณโดยใส่คำอธิบายประกอบและ/หรือกำหนดไฟล์ RBI ในทางกลับกัน ไฟล์ RBI ทำงานเป็นส่วนต่อประสานระหว่างส่วนประกอบสแตติกและไดนามิกโดยให้ "คำอธิบาย" ของไฟล์เหล่านี้ (ค่าคงที่ บรรพบุรุษ รหัสเมตาโปรแกรมมิง และอื่นๆ)
ดังนั้น หาก Sorbet เกี่ยวข้องกับการตรวจสอบแบบสถิตเป็นส่วนใหญ่ และ RBS ถูกสร้างขึ้นเพื่อจัดการกับการพิมพ์แบบไดนามิก อะไรคือความแตกต่างระหว่างพวกเขา? ทั้งสองจะอยู่ร่วมกันได้อย่างไร? ฉันควรใช้อันใดอันหนึ่งแทนอันอื่น
นี่เป็นคำถามทั่วไปเกี่ยวกับบทบาทหลักของ RBS นั่นเป็นเหตุผลที่เราตัดสินใจเขียนงานชิ้นนี้ เพื่อชี้แจง ในทางปฏิบัติ เหตุใดคุณจึงควรพิจารณาใช้โดยพิจารณาจากความสามารถ มาดำดิ่งกันเลย!
เริ่มต้นด้วยพื้นฐาน
มาเริ่มด้วยความเข้าใจที่ชัดเจนเกี่ยวกับความแตกต่างระหว่าง การพิมพ์แบบคงที่ _และ การพิมพ์แบบไดนามิก_. แม้ว่าจะเป็นพื้นฐาน แต่ก็เป็นแนวคิดหลักที่ต้องเข้าใจเพื่อให้เข้าใจถึงบทบาทของ RBS
ลองใช้ข้อมูลโค้ดจากภาษาที่พิมพ์แบบสแตติกเป็นข้อมูลอ้างอิง:
➜
String str = "";
str = 2.4;
ไม่เป็นข่าวว่าภาษาดังกล่าวสนใจประเภทของวัตถุและตัวแปร อย่างที่กล่าวไปแล้วว่าโค้ดแบบข้างบนจะทำให้เกิดข้อผิดพลาด
Ruby ก็เหมือนกับภาษาอื่นๆ หลายๆ ภาษา เช่น JavaScript, Python และ Objective-C ที่ไม่ค่อยให้ความสนใจกับประเภทที่คุณกำหนดเป้าหมายสำหรับออบเจกต์ของคุณมากนัก
รหัสเดียวกันใน Ruby จะทำงานสำเร็จดังที่แสดงด้านล่าง:
➜ irb
str = ""
str = 2.4
puts str # prints 2.4
เป็นไปได้เพราะล่ามของ Ruby รู้วิธีไดนามิก เปลี่ยนจากประเภทหนึ่งเป็นอีกประเภทหนึ่ง
อย่างไรก็ตาม มีข้อจำกัดสำหรับสิ่งที่ล่ามอนุญาต ตัวอย่างเช่น เปลี่ยนรหัสดังต่อไปนี้:
➜ irb
val = "6.0"
result = val + 2.0
puts result
ในทางกลับกัน จะทำให้เกิดข้อผิดพลาดดังต่อไปนี้:
ข้อผิดพลาด:ไม่มีการแปลง Float เป็นสตริงโดยนัย
ตัวอย่างเช่น การเรียกใช้โค้ดเดียวกันกับ JavaScript จะทำงานได้ดี
คุณธรรมของเรื่องราว:Ruby อนุมานประเภทแบบไดนามิกจริงๆ แต่ไม่เหมือนกับภาษาไดนามิกหลักอื่น ๆ มันไม่ยอมรับทุกอย่าง ระวังให้ดี!
และนั่นคือจุดที่ตัวตรวจสอบประเภท (ไม่ว่าจะเป็นแบบสแตติกหรือไดนามิก) มีประโยชน์
RBS กับ Sorbet
ใช่ ฉันมีประเด็นของคุณเกี่ยวกับไดนามิกกับสแตติก แต่แล้วเชอร์เบทล่ะ? จะเลิกใช้ไหม
ไม่เลย. ความแตกต่างหลัก (และอาจสำคัญที่สุด) ระหว่าง RBS กับ Sorbet คืออดีตเป็นเพียงภาษา ในขณะที่ตัวหลังเป็นตัวตรวจสอบประเภทที่สมบูรณ์
ทีม Ruby ยืนยันเป้าหมายหลักของ RBS ในการ อธิบายโครงสร้าง ของรหัสของคุณ มันจะไม่ทำการตรวจสอบประเภท แต่จะกำหนดโครงสร้างที่ตัวตรวจสอบประเภท (เช่น Sorbet หรืออื่น ๆ ) สามารถใช้ตรวจสอบประเภทได้เช่นกัน โครงสร้างโค้ดถูกเก็บไว้ในนามสกุลไฟล์ใหม่ — .rbs .
ลองใช้คลาส Ruby ต่อไปนี้เป็นตัวอย่าง:
class Super
def initialize(val)
@val = val
end
def val?
@val
end
end
class Test < Super
def initialize(val, flag)
super(val)
@flag = flag
end
def flag?
@flag
end
end
มันแสดงถึงการสืบทอดอย่างง่ายใน Ruby สิ่งที่น่าสนใจที่ควรทราบคือเราไม่สามารถเดาประเภทของแต่ละแอตทริบิวต์ที่ใช้ในชั้นเรียนได้ ยกเว้น flag
.
ตั้งแต่ flag
มาพร้อมกับค่าเริ่มต้น ทั้งผู้พัฒนาและตัวตรวจสอบประเภทสามารถสรุปประเภทเพื่อป้องกันการใช้งานในทางที่ผิดได้
ต่อไปนี้จะเป็นการแสดงที่เหมาะสมของคลาสข้างต้นในรูปแบบ RBS:
class Super
attr_reader val : untyped
def initialize : (val: untyped) -> void
end
class Test < Super
attr_reader flag : bool
def initialize : (val: untyped, ?flag: bool) -> void
def flag? : () -> bool
end
ใช้เวลาสักครู่เพื่อแยกแยะสิ่งนี้ เป็นภาษาประกาศ ดังนั้นเฉพาะลายเซ็นจึงอาจปรากฏในไฟล์ RBS ง่ายใช่มั้ย
ไม่ว่าจะสร้างโดยอัตโนมัติโดยเครื่องมือ CLI (เพิ่มเติมในภายหลัง) หรือโดยคุณ การใส่คำอธิบายประกอบประเภทเป็น untyped
จะปลอดภัยกว่า เมื่อคาดเดาไม่ได้
หากคุณแน่ใจเกี่ยวกับประเภทของ val
ตัวอย่างเช่น การแมป RBS ของคุณอาจเปลี่ยนเป็นข้อมูลต่อไปนี้:
class Super
attr_reader val : Integer
def initialize : (val: Integer) -> void
end
สิ่งสำคัญคือต้องทราบด้วยว่าทั้งทีม Ruby และ Sorbet กำลังทำงาน (และยังคงทำงานอยู่) เพื่อสร้างและปรับปรุง RBS ประสบการณ์ของทีม Sorbet ในการตรวจสอบประเภทเป็นเวลาหลายปีที่ช่วยให้ทีม Ruby ปรับแต่งสิ่งต่างๆ มากมายในโปรเจ็กต์นี้
การทำงานร่วมกันระหว่างไฟล์ RBS และ RBI ยังอยู่ระหว่างการพัฒนา เป้าหมายคือให้ซอร์เบตและเครื่องมือตรวจสอบอื่น ๆ มีพื้นฐานอย่างเป็นทางการและเป็นศูนย์กลางในการปฏิบัติตาม
เครื่องมือ RBS CLI
การพิจารณาที่สำคัญอย่างหนึ่งที่ทีม Ruby มีในการพัฒนา RBS คือการจัดส่งเครื่องมือ CLI ที่สามารถช่วยให้นักพัฒนาทดลองใช้งานและเรียนรู้วิธีใช้งาน เรียกว่า rbs และมาโดยค่าเริ่มต้นกับ Ruby 3 หากคุณยังไม่ได้อัปเกรดเวอร์ชัน Ruby ของคุณ คุณสามารถเพิ่มอัญมณีลงในโปรเจ็กต์ของคุณได้โดยตรงเช่นกัน:
➜ gem install rbs
คำสั่ง rbs help
จะแสดงการใช้คำสั่งพร้อมกับคำสั่งที่มีอยู่
รายการคำสั่งที่ใช้ได้
คำสั่งเหล่านี้ส่วนใหญ่เน้นที่การแยกวิเคราะห์และวิเคราะห์โครงสร้างโค้ด Ruby ตัวอย่างเช่น คำสั่ง ancestors
กวาดโครงสร้างลำดับชั้นของคลาสที่กำหนดเพื่อตรวจสอบบรรพบุรุษ:
➜ rbs ancestors ::String
::String
::Comparable
::Object
::Kernel
::BasicObject
คำสั่ง methods
แสดงโครงสร้างเมธอดทั้งหมดของคลาสที่กำหนด:
➜ rbs methods ::String
! (public)
!= (public)
!~ (public)
...
Array (private)
Complex (private)
Float (private)
...
autoload? (private)
b (public)
between? (public)
...
ต้องการดูโครงสร้างวิธีการเฉพาะหรือไม่? ไปหา method
:
➜ rbs method ::String split
::String#split
defined_in: ::String
implementation: ::String
accessibility: public
types:
(?::Regexp | ::string pattern, ?::int limit) -> ::Array[::String]
| (?::Regexp | ::string pattern, ?::int limit) { (::String) -> void } -> self
สำหรับผู้ที่เริ่มต้นด้วย RBS วันนี้ คำสั่ง prototype
สามารถช่วยได้มากกับประเภทนั่งร้านสำหรับชั้นเรียนที่มีอยู่แล้ว คำสั่งสร้างต้นแบบของไฟล์ RBS
มาดู Test < Super
ก่อนหน้ากัน ตัวอย่างการสืบทอดและบันทึกรหัสลงในไฟล์ชื่อ appsignal.rb . จากนั้นรันคำสั่งต่อไปนี้:
➜ rbs prototype rb appsignal.rb
เนื่องจากคำสั่งอนุญาตให้ rb , rbi และ รันไทม์ เครื่องกำเนิดไฟฟ้า คุณต้องระบุประเภทของไฟล์ที่คุณกำลังนั่งร้านอยู่หลัง prototype
คำสั่ง ตามด้วยชื่อพาธของไฟล์
ต่อไปนี้เป็นผลลัพธ์ของการดำเนินการ:
class Super
def initialize: (untyped val) -> untyped
def val?: () -> untyped
end
class Test < Super
def initialize: (untyped val, ?flag: bool flag) -> untyped
def flag?: () -> untyped
end
ค่อนข้างคล้ายกับ RBS เวอร์ชันแรกของเรา ตามที่กล่าวไว้ก่อนหน้านี้ เครื่องมือจะทำเครื่องหมายเป็น untyped
ประเภทใดที่คาดเดาไม่ได้
นอกจากนี้ยังนับสำหรับการส่งคืนเมธอด สังเกตประเภทการส่งคืนของ flag
คำนิยาม. ในฐานะนักพัฒนา คุณอาจแน่ใจว่าวิธีการส่งคืนบูลีนเสมอ แต่เนื่องจากลักษณะไดนามิกของ Ruby เครื่องมือจึงไม่สามารถบอกได้ 100% ว่าเป็นเช่นนั้น
และนั่นคือตอนที่เด็ก Ruby 3 อีกคนมาช่วย:TypeProf
เครื่องมือ TypeProf
TypeProf เป็นเครื่องมือวิเคราะห์ประเภทสำหรับ Ruby ที่สร้างขึ้นจากการตีความแผนผังไวยากรณ์บางส่วน
แม้ว่าจะยังคงอยู่ในขั้นทดลอง แต่ก็พิสูจน์แล้วว่ามีประสิทธิภาพมากในการทำความเข้าใจว่าโค้ดของคุณพยายามทำอะไร
หากคุณยังไม่มี Ruby 3 เพียงเพิ่มอัญมณีลงในโปรเจ็กต์ของคุณ:
➜ gem install typeprof
ตอนนี้ ให้เรียกใช้ appsignal.rb . เดียวกัน ฟ้องมัน:
➜ typeprof appsignal.rb
นี่คือผลลัพธ์:
# Classes
class Super
@val: untyped
def initialize: (untyped) -> untyped
def val?: -> untyped
end
class Test < Super
@val: untyped
@flag: true
def initialize: (untyped, ?flag: true) -> true
def flag?: -> true
end
สังเกตว่า flag
ถูกแมปแล้ว สิ่งนี้เป็นไปได้เพียงเพราะไม่เหมือนกับต้นแบบ RBS ที่ทำ TypeProf จะสแกนเนื้อความของเมธอดเพื่อทำความเข้าใจว่าการกระทำใดกำลังดำเนินการกับตัวแปรเฉพาะนั้น เนื่องจากไม่สามารถระบุการเปลี่ยนแปลงโดยตรงใดๆ กับตัวแปรนี้ได้ TypeProf จึงสามารถแมปเมธอดที่ส่งคืนเป็นบูลีนได้อย่างปลอดภัย
ตัวอย่างเช่น ลองพิจารณาว่า TypeProf จะสามารถเข้าถึงคลาสอื่นที่สร้างอินสแตนซ์และใช้ Test
ระดับ. ด้วยสิ่งนี้ โค้ดของคุณจึงสามารถเจาะลึกลงไปในโค้ดของคุณและปรับแต่งการคาดคะเนได้อย่างละเอียด สมมติว่ามีการเพิ่มข้อมูลโค้ดต่อไปนี้ที่ส่วนท้ายของ appsignal.rb ไฟล์:
testSub = Test.new("My value", "My value" == "")
testSup = Super.new("My value")
และคุณเปลี่ยน initialize
วิธีการลงนามดังต่อไปนี้:
def initialize(val, flag)
เมื่อคุณรันคำสั่งอีกครั้ง สิ่งนี้ควรเป็นผลลัพธ์:
# Classes
class Super
@val: String
def initialize: (String) -> String
def val?: -> String
end
class Test < Super
@val: String
@flag: bool
def initialize: (String val, bool flag) -> bool
def flag?: -> bool
end
เจ๋งมาก!
TypeProf ไม่สามารถจัดการกับแอตทริบิวต์ที่สืบทอดมาได้เป็นอย่างดี นั่นเป็นเหตุผลที่เรากำลังสร้าง Super
. ใหม่ วัตถุ. มิฉะนั้น จะไม่ได้รับ val
. นั้น เป็น String
.
ข้อดีหลักของ TypeProf คือความปลอดภัย เมื่อใดก็ตามที่ไม่สามารถหาสิ่งที่แน่นอนได้ ให้ untyped
จะถูกส่งคืน
ข้อกำหนด RBS บางส่วน
คำเตือนที่สำคัญอย่างหนึ่งจากเอกสารอย่างเป็นทางการระบุว่าแม้ว่า TypeProf จะมีประสิทธิภาพมาก แต่คุณควรตระหนักถึงข้อจำกัดเกี่ยวกับสิ่งที่สามารถสร้างและไม่สามารถสร้างในแง่ของรหัส RBS
ตัวอย่างเช่น แนวทางปฏิบัติทั่วไปในหมู่นักพัฒนา Ruby คือวิธีการโอเวอร์โหลด ซึ่งคุณเรียกใช้พฤติกรรมที่แตกต่างกันของวิธีการขึ้นอยู่กับอาร์กิวเมนต์
พิจารณาว่าเป็นวิธีการใหม่ spell
ถูกเพิ่มใน Super
class ซึ่งส่งคืน Integer
หรือ String
ตามประเภทพารามิเตอร์:
def spell(val)
if val.is_a?(String)
""
else
0
end
end
RBS รวบรวมแนวปฏิบัตินี้โดยอนุญาตให้คุณจัดการกับการโอเวอร์โหลดผ่านประเภทสหภาพ (ค่าที่แสดงถึงรูปแบบที่เป็นไปได้หลายประเภท):
def spell: (String) -> String | (Integer) -> Integer
TypeProf ไม่สามารถอนุมานได้โดยการวิเคราะห์เนื้อความของวิธีการ เพื่อช่วยในเรื่องนี้ คุณสามารถเพิ่มคำจำกัดความดังกล่าวลงในไฟล์ RBS ของคุณได้ด้วยตนเอง และ TypeProf จะตรวจสอบคำแนะนำที่นั่นก่อนเสมอ
สำหรับสิ่งนี้ คุณต้องเพิ่มพาธไฟล์ RBS ที่ส่วนท้ายของคำสั่ง:
typeprof appsignal.rb appsignal.rbs
ดูผลลัพธ์ใหม่ด้านล่าง:
class Super
...
def spell: (untyped val) -> (Integer | String)
end
นอกจากนี้เรายังสามารถตรวจสอบประเภทจริงระหว่างรันไทม์ผ่าน Kernel#p
เพื่อทดสอบว่าการโอเวอร์โหลดทำงานโดยเพิ่มสองบรรทัดถัดไปที่ส่วนท้ายของ appsignal.rb ไฟล์:
p testSup.spell(42)
p testSup.spell("str")
นี่ควรเป็นผลลัพธ์:
# Revealed types
# appsignal.rb:11 #=> Integer
# appsignal.rb:12 #=> String
...
อย่าลืมอ้างอิงเอกสารอย่างเป็นทางการสำหรับข้อมูลเพิ่มเติม โดยเฉพาะส่วนที่เกี่ยวข้องกับข้อจำกัดของ TypeProf
พิมพ์เป็ด
คุณเคยได้ยินเรื่องนี้มาก่อน หากวัตถุ Ruby ทำทุกอย่างที่เป็ดทำ สิ่งนั้นก็คือเป็ด
ดังที่เราได้เห็นแล้ว Ruby ไม่สนใจว่าสิ่งของของคุณจะเป็นอย่างไร ประเภทสามารถเปลี่ยนแปลงไดนามิกเช่นเดียวกับการอ้างอิงวัตถุ
แม้ว่าจะมีประโยชน์ แต่การพิมพ์เป็ดก็อาจทำได้ยาก มาดูตัวอย่างกัน
สมมุติว่าจากนี้ไป val
แอตทริบิวต์ที่คุณได้ประกาศสำหรับ Super
คลาส ซึ่งเป็น String
จะต้องแปลงเป็นจำนวนเต็มได้เสมอ
แทนที่จะเชื่อมั่นว่านักพัฒนาซอฟต์แวร์จะรับประกัน Conversion เสมอ (อาจเกิดข้อผิดพลาดขึ้น) คุณสามารถสร้างอินเทอร์เฟซ โดยระบุว่า:
interface _IntegerConvertible
def to_int: () -> Integer
end
ชนิดของอินเทอร์เฟซมีอย่างน้อยหนึ่งวิธีที่แยกออกจากคลาสและโมดูลที่เป็นรูปธรรม ด้วยวิธีนี้ เมื่อคุณต้องการส่งต่อบางประเภทไปยังอินสแตนซ์ Super คุณสามารถทำสิ่งต่อไปนี้ได้:
class Super
attr_reader val : _IntegerConvertible
def initialize : (val: _IntegerConvertible) -> void
end
คลาสหรือโมดูลที่เป็นรูปธรรมที่ใช้อินเทอร์เฟซนี้จะต้องตรวจสอบให้แน่ใจว่ามีการตรวจสอบความถูกต้องถูกต้อง
เมตาโปรแกรมมิ่ง
บางทีหนึ่งในคุณสมบัติแบบไดนามิกที่สุดของ Ruby ก็คือความสามารถในการสร้างโค้ดที่สร้างโค้ดด้วยตัวเองระหว่างรันไทม์ นั่นคือเมตาโปรแกรมมิ่ง
เนื่องจากธรรมชาติของสิ่งต่าง ๆ ที่ไม่แน่นอน เครื่องมือ RBS CLI จึงไม่สามารถสร้าง RBS จากโค้ดโปรแกรมเมตาได้
ลองใช้ตัวอย่างต่อไปนี้เป็นตัวอย่าง:
class Test
define_method :multiply do |*args|
args.inject(1, :*)
end
end
p Test.new.multiply(2, 3, 5)
คลาสนี้กำหนดวิธีการที่เรียกว่า multiply
ที่รันไทม์และสั่งให้ฉีดอาร์กิวเมนต์และคูณแต่ละอาร์กิวเมนต์ด้วยผลลัพธ์ก่อนหน้า
เมื่อคุณเรียกใช้ RBS prototype
คำสั่ง นี่ควรเป็นผลลัพธ์:
class Test
end
ขึ้นอยู่กับความซับซ้อนของโค้ด metaprogramming ของคุณ TypeProf จะพยายามอย่างดีที่สุดเพื่อดึงข้อมูลบางอย่างออกจากมัน แต่ก็ไม่ได้รับประกันเสมอไป
จำไว้ว่าคุณสามารถเพิ่มการแมปประเภทของคุณเองลงในไฟล์ RBS ได้เสมอ และ TypeProf จะเชื่อฟังพวกเขาล่วงหน้า นั่นก็ใช้ได้กับ metaprogramming เช่นกัน
สิ่งสำคัญคือต้องอัปเดตการเปลี่ยนแปลงที่เก็บล่าสุดอยู่เสมอ เนื่องจากทีมมีการเปิดตัวคุณลักษณะใหม่ๆ อยู่เสมอ ซึ่งอาจรวมถึงการอัปเดตเกี่ยวกับเมตาโปรแกรมด้วย
ดังที่กล่าวไปแล้ว หาก codebase ของคุณมี metaprogramming บางประเภท ให้ระมัดระวังเครื่องมือเหล่านี้ อย่าใช้มันสุ่มสี่สุ่มห้า!
บทสรุป
มีรายละเอียดเพิ่มเติมมากมายเกี่ยวกับสิ่งที่เราได้พูดคุยไปแล้ว รวมถึงกรณีการใช้งาน edge สำหรับ RBS และ TypeProf ที่คุณควรทราบ
ดังนั้น อย่าลืมอ้างอิงเอกสารอย่างเป็นทางการสำหรับข้อมูลเพิ่มเติม
RBS นั้นยังใหม่อยู่มาก แต่ได้สร้างผลกระทบอย่างใหญ่หลวงต่อ Rubyists ที่ใช้พิมพ์ตรวจสอบฐานโค้ดด้วยเครื่องมืออื่นๆ
แล้วคุณล่ะ คุณลองแล้วหรือยัง? คุณคิดอย่างไรกับ RBS
ป.ล. หากคุณต้องการอ่านโพสต์ Ruby Magic ทันทีที่ออกจากสื่อ สมัครรับจดหมายข่าว Ruby Magic ของเราและไม่พลาดแม้แต่โพสต์เดียว!