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

ทำความเข้าใจ RBS, Rubys ใหม่ Type Annotation System

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

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

การให้คำอธิบายประกอบประเภทกับ RBS ทำให้คุณได้รับประโยชน์ต่างๆ เช่น:

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

แต่เดี๋ยวก่อน! ไม่มีตัวตรวจสอบประเภทคงที่เช่น Sorbet และ Steep หรือไม่

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

RBS เป็นภาษาทางการและกำลังจะมีชีวิตพร้อมกับ Ruby 3

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

ติดตั้ง Ruby 3

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

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

หรือคุณสามารถติดตั้ง rbs อัญมณีโดยตรงในโครงการปัจจุบันของคุณ:

gem install rbs

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

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

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

ซึ่งตรงกันข้ามกับสิ่งที่คาดหวังจากการพิมพ์แบบคงที่ ภาษาที่พิมพ์แบบสแตติก เช่น Java และ C จะตรวจสอบประเภทระหว่างเวลาที่คอมไพล์

ใช้ข้อมูลโค้ด Java ต่อไปนี้เป็นข้อมูลอ้างอิง:

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

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

error: incompatible types: String cannot be converted to int

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

number = 0;
number = "Hi, number!";
puts number // Successfully prints "Hi, number!"

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

แนวคิดนั้นมักสับสนกับ พิมพ์ดีด vs พิมพ์ไม่เก่ง ภาษา

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 // it concatenates both values

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

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

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

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

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

มาดูตัวอย่างการสืบทอด 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 . หนึ่งตัว พารามิเตอร์และไม่ส่งคืนสิ่งใด

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

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

เครื่องมือนั่งร้าน

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

เมื่อคำนึงถึงสิ่งนี้ ทีม Ruby ได้จัดเตรียมเครื่องมือ CLI ที่เป็นประโยชน์ชื่อ rbs กับประเภทนั่งร้านสำหรับชั้นเรียนที่มีอยู่

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

ทำความเข้าใจ RBS, Rubys ใหม่ Type Annotation System คำสั่งที่มีอยู่ในเครื่องมือ CLI

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

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

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

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

คลาสง่ายๆ ที่มีคุณลักษณะและเมธอด โปรดทราบว่าหนึ่งในนั้นจะมีค่าบูลีนเริ่มต้น ซึ่งเป็นสิ่งสำคัญในการแสดงให้เห็นว่า 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

เมื่อพูดถึง metaprogramming rbs เครื่องมือจะไม่ช่วยอะไรมากเนื่องจากมีลักษณะแบบไดนามิก

ใช้ชั้นเรียนต่อไปนี้เป็นตัวอย่าง:

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

Meta.new.greeting

ต่อไปนี้จะเป็นผลของการนั่งร้านประเภทนี้:

class Meta
end

พิมพ์เป็ด

Ruby ไม่ได้กังวลมากนักเกี่ยวกับธรรมชาติของวัตถุ (ประเภทของวัตถุ) แต่สนใจในสิ่งที่พวกเขาสามารถทำได้ (สิ่งที่พวกเขาทำ)

การพิมพ์เป็ดเป็นรูปแบบการเขียนโปรแกรมที่มีชื่อเสียงที่ทำงานตามคำขวัญ:

"ถ้าสิ่งของมีพฤติกรรมเหมือนเป็ด (พูด เดิน บิน ฯลฯ) สิ่งนั้นก็คือเป็ด"

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

อย่างไรก็ตาม การพิมพ์แบบเป็ดอาจซ่อนรายละเอียดของการใช้งานโค้ดที่อาจยุ่งยากและค้นหา/อ่านได้ยาก

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

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

class Terrestrial < Animal
    def initialize(weight)
        super(weight)
    end

    def run
        puts "Running..."
    end
end

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

interface _CanRun
  # Requires `<<` operator which accepts `Terrestrial` object.
  def <<: (Terrestrial) -> void
end

เมื่อทำการแมปโค้ด RBS กับวิธีการรันเฉพาะ นั่นคือลายเซ็น:

def run: (_CanRun) -> void

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

ประเภทของสหภาพ

เป็นเรื่องปกติในหมู่ Rubyists ที่จะมีนิพจน์ที่มีค่าประเภทต่างๆ

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

RBS รองรับประเภทสหภาพโดยเพียงแค่เข้าร่วมผ่าน pipe โอเปอเรเตอร์

วิธีการโอเวอร์โหลด

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

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

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

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

TypeProf

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

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

ในการติดตั้ง เพียงเพิ่มอัญมณีลงในโปรเจ็กต์ของคุณ:

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

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

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

ทำความเข้าใจ RBS, Rubys ใหม่ Type Annotation System การเน้นไวยากรณ์ RBS ใน VS Code

บทสรุป

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

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

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