มาเริ่มโพสต์นี้ด้วยเกมทายผลสนุกๆ กัน คุณคิดว่าอะไรคือข้อผิดพลาดที่พบบ่อยที่สุดที่ 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 ตอนนี้เป็นเวลาที่ดีที่จะมีส่วนร่วม!