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

RBS:ภาษาพิมพ์ใหม่ Ruby 3 ในการใช้งานจริง

ในที่สุด 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 ของเราและไม่พลาดแม้แต่โพสต์เดียว!