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

พิมพ์ เช็คอิน Ruby — ตรวจสอบ Yo Self ก่อนที่คุณจะทำลาย Yo Self

มาเริ่มโพสต์นี้ด้วยเกมทายผลสนุกๆ กัน คุณคิดว่าอะไรคือข้อผิดพลาดที่พบบ่อยที่สุดที่ AppSignal ติดตามในแอปพลิเคชัน Ruby

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

ประเภทการช่วยเหลือ?

ยกเว้นการเลือกตัวแก้ไขข้อความหรือภาษาโปรแกรม มีบางหัวข้อที่สามารถนำไปสู่การอภิปรายที่ดุเดือดได้เร็วกว่าการอภิปรายเกี่ยวกับระบบประเภท เราจะไม่มีเวลาลงรายละเอียดที่นี่ แต่โพสต์ของ Chris Smith เรื่อง "สิ่งที่ต้องรู้ก่อนระบบประเภทการโต้วาที" ทำได้ดีมาก

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

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

พิมพ์เป็ด

Ruby เป็นภาษาที่ตรวจสอบการพิมพ์แบบไดนามิกและปฏิบัติตามแนวทาง "การพิมพ์เป็ด":

ถ้ามันเดินเหมือนเป็ดและร้องเหมือนเป็ด มันต้องเป็ด

หมายความว่าโดยทั่วไปนักพัฒนา Ruby ไม่ได้กังวลเกี่ยวกับประเภทของวัตถุมากนัก แต่จะตอบสนองต่อ "ข้อความ" (หรือวิธีการบางอย่าง) หรือไม่

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

  • ความถูกต้อง:การพิมพ์แบบคงที่ช่วยป้องกัน บางคลาส ของจุดบกพร่องต่างๆ เช่น NoMethodError . ที่กล่าวมา .
  • การติดเครื่องมือ:บ่อยครั้ง การมีข้อมูลประเภทคงที่ในระหว่างการพัฒนาจะนำไปสู่ตัวเลือกเครื่องมือที่ดีขึ้น (เช่น การรองรับการปรับโครงสร้างใหม่ใน IDE เป็นต้น)
  • เอกสารประกอบ:ภาษาที่พิมพ์แบบสแตติกจำนวนมากมีเครื่องมือจัดทำเอกสารในตัวที่ยอดเยี่ยม Haskell's Hoogle ใช้สิ่งนี้เพื่อสร้างผลลัพธ์ที่ยอดเยี่ยมโดยนำเสนอเครื่องมือค้นหาที่สามารถค้นหาฟังก์ชันตามประเภทลายเซ็นได้
  • ประสิทธิภาพ:ยิ่งมีข้อมูลสำหรับคอมไพเลอร์มากเท่าใด การปรับประสิทธิภาพให้เหมาะสมที่สามารถนำมาใช้ได้มากขึ้นเท่านั้น

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

การตรวจสอบประเภททีละน้อย

ในช่วงไม่กี่ปีที่ผ่านมา แนวทางที่เรียกกันทั่วไปว่า "การตรวจสอบประเภททีละน้อย" ได้ขยายไปสู่ภาษาที่ตรวจสอบประเภทแบบไดนามิกต่างๆ:ตั้งแต่ TypeScript สำหรับ JS ไปจนถึง Hack for PHP และ mypy สำหรับ Python สิ่งที่แนวทางเหล่านี้มีเหมือนกันคือไม่ต้องใช้วิธีการทั้งหมดหรือไม่มีเลย แต่ให้นักพัฒนาค่อยๆ เพิ่มข้อมูลประเภทลงในตัวแปรและนิพจน์ตามที่เห็นสมควร สิ่งนี้มีประโยชน์อย่างยิ่งสำหรับ codebase ขนาดใหญ่ที่มีอยู่ ซึ่งสามารถตรวจสอบส่วนที่สำคัญที่สุดของระบบแบบสแตติกได้ในขณะที่ยังไม่ได้พิมพ์ส่วนที่เหลือและตรวจสอบตอนรันไทม์ โซลูชันการตรวจสอบประเภททั้งหมดสำหรับ Ruby ที่เราจะสำรวจในส่วนที่เหลือของบทความนี้ใช้แนวทางเดียวกัน

ตัวเลือก

หลังจากพิจารณาว่าทำไมนักพัฒนา Ruby อาจต้องการเพิ่มการตรวจสอบประเภทสแตติกในเวิร์กโฟลว์การพัฒนาแล้ว ก็ถึงเวลาสำรวจตัวเลือกยอดนิยมบางตัวในปัจจุบันสำหรับการทำเช่นนั้น อย่างไรก็ตาม สิ่งสำคัญที่ควรทราบคือแนวคิดในการเพิ่มการตรวจสอบประเภทสแตติกใน Ruby ไม่ใช่เรื่องใหม่ นักวิจัยจากมหาวิทยาลัยแมริแลนด์ทำงานเกี่ยวกับส่วนขยาย Ruby ชื่อ Diamondback Ruby (Druby) ในช่วงต้นปี 2009 และ Tufts University Programming Language Group ได้เผยแพร่บทความชื่อ The Ruby Type Checker ในปี 2013 ซึ่งในที่สุดก็นำไปสู่โครงการ RDL ซึ่งเสนอประเภท ความสามารถในการตรวจสอบและออกแบบตามสัญญาในฐานะห้องสมุด

เชอร์เบท

Sorbet ได้รับการพัฒนาโดย Stripe ปัจจุบันเป็นโซลูชันการตรวจสอบประเภทที่มีคนพูดถึงมากที่สุดสำหรับ Ruby ไม่น้อยเพราะบริษัทใหญ่ๆ เช่น Shopify, GitLab, Kickstarter และ Coinbase เป็นผู้เริ่มใช้งานในช่วงเบต้าแบบปิด เดิมทีมีการประกาศในช่วง Ruby Kaigi ปีที่แล้วและเปิดตัวสู่สาธารณะครั้งแรกในวันที่ 20 มิถุนายนของปีนี้ Sorbet เขียนด้วย C ++ ที่ทันสมัยและแม้จะมีการตั้งค่าของ Matz (คำพูด:"ฉันเกลียดคำอธิบายประกอบประเภท") เลือกใช้วิธีการตามคำอธิบายประกอบประเภท สิ่งที่น่าสนใจเป็นพิเศษเกี่ยวกับ Sorbet ก็คือการเลือกใช้การตรวจสอบประเภทสแตติกและไดนามิกร่วมกัน เนื่องจากลักษณะไดนามิกสุดขีดของ Ruby และความสามารถในการเขียนโปรแกรมเมตานั้นเป็นสิ่งที่ท้าทายสำหรับระบบประเภทสแตติก

# typed: true
class Test
  extend T::Sig
 
  sig {params(x: Integer).returns(String)}
  def to_s(x)
    x.to_s
  end
end

หากต้องการเปิดใช้งานการตรวจสอบประเภท เราต้องเพิ่ม # typed: true . ก่อน แสดงความคิดเห็นวิเศษและขยายชั้นเรียนของเราด้วย T::Sig โมดูล. คำอธิบายประกอบประเภทจริงถูกระบุด้วย sig วิธีการ:

sig {params(x: Integer).returns(String)}

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

Test.new.to_s("42")
# Expected Integer but found String("42") for argument x

นอกเหนือจากการตรวจสอบพื้นฐานเหล่านี้แล้ว Sorbet ยังมีเคล็ดลับเพิ่มเติมอีกเล็กน้อย ตัวอย่างเช่น มันสามารถช่วยเราให้พ้นจาก NoMethodError . อันน่าสะพรึงกลัว เมื่อ nil :

users = T::Array[User].new
user = users.first
user.username
 
# Method username does not exist on NilClass component of T.nilable(User)

ข้อมูลโค้ดด้านบนกำหนดอาร์เรย์ว่างของ User วัตถุและเมื่อเราพยายามเข้าถึงองค์ประกอบแรก (ซึ่งจะกลับ nil ) Sorbet เตือนเราอย่างถูกต้องว่าไม่มีเมธอดชื่อ username มีอยู่ใน NilClass . อย่างไรก็ตาม หากเราแน่ใจว่าค่าใดค่าหนึ่งไม่มีวันเป็น nil เราสามารถใช้ T.must เพื่อให้ซอร์เบต์รู้สิ่งนี้:

users = T::Array[User].new
user = T.must(users.first)
user.username

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

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

นอกจากนี้ Sorbet ยังสามารถทำงานกับไฟล์ "Ruby Interface" (rbi ) ซึ่งเก็บไว้ใน sorbet/ โฟลเดอร์ในไดเร็กทอรีการทำงานปัจจุบันของคุณ ซึ่งช่วยให้เราสร้างคำจำกัดความของอินเทอร์เฟซสำหรับอัญมณีทั้งหมดที่โปรเจ็กต์ใช้ ซึ่งช่วยให้เราค้นหาข้อผิดพลาดประเภทเพิ่มเติมได้

Sorbet มีอะไรมากกว่าที่เราจะพูดถึงในบทความเดียว (เช่น ระดับความเข้มงวดที่แตกต่างกันหรือปลั๊กอิน metaprogramming) แต่เอกสารของ Sorbet นั้นค่อนข้างดีอยู่แล้วและเปิดให้ประชาสัมพันธ์ได้

สูงชัน

ทางเลือกที่เป็นที่รู้จักกันอย่างแพร่หลายมากที่สุดสำหรับซอร์เบต์คือ Steep โดย Soutaro Matsumoto ไม่ใช้คำอธิบายประกอบและไม่ทำการอนุมานประเภทใดๆ ด้วยตัวเอง แต่อาศัย .rbi . แทน ไฟล์ใน sig ไดเรกทอรี

เริ่มจากคลาส Ruby ง่ายๆ ต่อไปนี้:

class User
  attr_reader :first_name, :last_name, :address
 
  def initialize(first_name, last_name, address)
    @first_name = first_name
    @last_name = last_name
    @address = address
  end
 
  def full_name
    "#{first_name} #{last_name}"
  end
end

ตอนนี้เราสามารถสร้าง user.rbi . เริ่มต้นได้ ไฟล์ด้วยคำสั่งต่อไปนี้:

$ steep scaffold user.rb > sig/user.rbi

ส่งผลให้ไฟล์ต่อไปนี้มีจุดมุ่งหมายเป็นจุดเริ่มต้น (แสดงโดยข้อเท็จจริงที่ทุกประเภทได้รับการระบุเป็น any ซึ่งไม่มีความปลอดภัย):

class User
  @first_name: any
  @last_name: any
  @address: any
  def initialize: (any, any, any) -> any
  def full_name: () -> String
end

อย่างไรก็ตาม หากเราพยายามพิมพ์เช็ค ณ จุดนี้ เราจะพบข้อผิดพลาดบางประการ:

$ steep check
user.rb:11:7: NoMethodError: type=::User, method=first_name (first_name)
user.rb:11:21: NoMethodError: type=::User, method=last_name (last_name)

เหตุผลที่เราเห็นสิ่งเหล่านี้คือ Steep ต้องการความคิดเห็นพิเศษเพื่อให้ทราบว่ามีการกำหนดวิธีการใดบ้างผ่าน attr_reader s ให้เพิ่มว่า:

# @dynamic first_name, last_name, address
attr_reader :first_name, :last_name, :address

นอกจากนี้ เราจำเป็นต้องเพิ่มคำจำกัดความสำหรับวิธีการสร้าง .rbi ไฟล์. ระหว่างที่เรากำลังดำเนินการอยู่ เรามาเปลี่ยนลายเซ็นจาก any . กัน ถึงประเภทที่แท้จริง:

class User
  @first_name: String
  @last_name: String
  @address: Address
  def initialize: (String, String, Address) -> any
  def first_name: () -> String
  def last_name: () -> String
  def address: () -> Address
  def full_name: () -> String
end

ตอนนี้ทุกอย่างทำงานได้ตามที่คาดไว้และ steep check ไม่ส่งคืนข้อผิดพลาดใดๆ

เหนือสิ่งอื่นใดที่เราได้เห็นจนถึงตอนนี้ Steep ยังรองรับข้อมูลทั่วไป (เช่น Hash<Symbol, String> ) และประเภทสหภาพ ซึ่งแสดงถึงตัวเลือกอย่างใดอย่างหนึ่งหรือระหว่างหลายประเภท ตัวอย่างเช่น top_post . ของผู้ใช้ วิธีสามารถส่งคืนโพสต์อันดับสูงสุดที่เขียนโดยผู้ใช้หรือ nil ถ้าพวกเขายังไม่ได้บริจาคอะไรเลย ซึ่งแสดงผ่านประเภทสหภาพ (Post | nil) และลายเซ็นที่เกี่ยวข้องจะมีลักษณะดังนี้:

def top_post: () -> (Post | nil)

แม้ว่า Steep จะมีคุณสมบัติน้อยกว่า Sorbet อย่างแน่นอน แต่ก็ยังเป็นเครื่องมือที่มีประโยชน์และดูเหมือนว่าจะสอดคล้องกับสิ่งที่ Matz จินตนาการไว้ในการตรวจสอบประเภท Ruby 3 มากกว่า

ตัวสร้างโปรไฟล์ประเภททับทิม

Yusuke Endoh (รู้จักกันดีในนาม "mame" ในแวดวงนักพัฒนา Ruby) จาก Cookpad กำลังทำงานเกี่ยวกับตัวตรวจสอบประเภทระดับ 1 ที่เรียกว่า Ruby Type Profiler ไม่เหมือนกับโซลูชันอื่น ๆ ที่นำเสนอที่นี่ ไม่ต้องการไฟล์ลายเซ็นหรือคำอธิบายประกอบประเภท แต่พยายามอนุมานเกี่ยวกับโปรแกรม Ruby ให้มากที่สุดในขณะที่แยกวิเคราะห์ แม้ว่าจะจับปัญหาที่อาจเกิดขึ้นได้น้อยกว่า Steep หรือ Sorbet แต่ก็ไม่มีค่าใช้จ่ายเพิ่มเติมสำหรับนักพัฒนา

สรุป

แม้ว่าจะไม่มีใครคาดเดาอนาคตได้ แต่ดูเหมือนว่าการตรวจสอบประเภทใน Ruby เป็นสิ่งที่ควรอยู่ ขณะนี้ มีความพยายามในการสร้างมาตรฐานสำหรับ "ภาษาลายเซ็นทับทิม" เพื่อใช้ใน .rbi ไฟล์ (อาจนั่งร้านโดย Ruby Type Profiler) ดังนั้นนักพัฒนาจึงสามารถใช้เครื่องมือใดก็ได้ที่ต้องการ Steep อนุญาตให้ผู้เขียนห้องสมุดจัดส่งข้อมูลประเภทด้วยอัญมณีของตนแล้ว และ Sorbet มีกลไกที่คล้ายกันในรูปแบบของซอร์เบต์ ซึ่งได้รับแรงบันดาลใจจากพื้นที่เก็บข้อมูล SureTyped สำหรับคำจำกัดความของ TypeScript หากคุณสนใจที่จะช่วยกำหนดอนาคตของการตรวจสอบประเภทใน Ruby ตอนนี้เป็นเวลาที่ดีที่จะมีส่วนร่วม!