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

แนะนำ RBS: ภาษา Typing ใหม่ของ Ruby 3 สำหรับ Code Reliability ที่แข็งแกร่งกว่า

Version 3.0.0 ของ Ruby ที่รอคอยมานานได้ Release สุดท้ายแล้ว พร้อมกับการปรับปรุงยอดเยี่ยมมากมาย เช่น Performance Boost 3x เทียบกับ Version ก่อนหน้า Concurrency-Parallel Experimental Features ฯลฯ ทีม Ruby ยัง Introduce Syntax Language ใหม่สำหรับ Dynamic Typing ใน Ruby: RBS

นี่เป็นสิ่งที่ทีมได้ Discuss มาหลายปี โดยอิงจาก Success ของ Community-developed Tools สำหรับ Static Type Checking เหมือน Sorbet

Sorbet เป็น Powerful Type Checker ที่ Backed โดย Stripe มัน Check โค้ดของคุณโดย Annotating และ/หรือ Defining RBI Files RBI Files ต่อมาทำงานเป็น Interfaces ระหว่าง Static และ Dynamic Components 提供 "Description" ของพวกเขา (Constants, Ancestors, Metaprogramming Code และมากกว่า)

ดังนั้น หาก Sorbet จัดการ Static Checking ส่วนใหญ่ และ RBS ถูกสร้างมาเพื่อ Address Dynamic Typing ความแตกต่างระหว่างพวกเขาคืออะไร? พวกเขาจะ Coexist ได้อย่างไร? เมื่อไหร่ควรใช้ตัวไหนแทนอีกตัว?

นี่คือคำถามที่ค่อนข้าง Common เกี่ยวกับ Major Role ของ RBS นั่นคือเหตุผลที่เราตัดสินใจเขียน Piece นี้ เพื่อ Clarify ในทางปฏิบัติว่าทำไมคุณควรพิจารณา Adopt มันบนสิ่งที่มัน Capable ของ มาเจาะลึกทันที!

เริ่มจาก Basics

มาเริ่มด้วย Clear Understanding ของความแตกต่างระหว่าง Static Typing _ and Dynamic Typing _ แม้จะ Basic มันเป็น Key Concept ที่ต้องเข้าใจเพื่อเข้าใจ Role ของ RBS

มา取 Code Snippet จาก Statically Typed Language เป็น Reference:

➜ String str = ""; str = 2.4;

ไม่ใช่ข่าวใหม่ที่ภาษาแบบนั้น Care สำหรับ Types ของ Objects และ Variables ของมัน That being said โค้ดแบบด้านบนจะ Throw Error

Ruby เหมือนหลายภาษาอื่นๆ เช่น JavaScript, Python และ Objective-C ไม่ให้ Attention มากนักเกี่ยวกับ Types ไหนที่คุณ Target สำหรับ Objects ของคุณ

โค้ดเดียวกันใน Ruby จะแค่ Run สำเร็จ เหมือนด้านล่าง:

➜ irb str = "" str = 2.4 puts str # prints 2.4

สิ่งนี้ Possible เพราะ Ruby's Interpreter รู้วิธี Dynamically Switch จาก Type หนึ่งไปอีก Type

อย่างไรก็ตาม มี Limit ที่ Interpreter อนุญาต ตัวอย่าง ให้ใช้ Code Change ต่อไปนี้:

➜ irb val = "6.0" result = val + 2.0 puts result

นี่จะ Produce Error ต่อไปนี้:

Error: no implicit conversion of Float into String

การ Run โค้ดเดียวกันกับ JavaScript ตัวอย่าง จะ Run Fine แค่ไหน

Moral of the story: Ruby Does Infer Types Dynamically จริงๆ แต่ ไม่เหมือน Dynamic Languages สำคัญอื่นๆ มันจะไม่ Accept ทุกอย่าง ให้ระวังเรื่องนั้น!

และนั่นคือที่ที่ Type Checkers (ไม่ว่าจะ Static หรือ Dynamic) กลายเป็น Useful

RBS vs Sorbet

ใช่ ฉันได้ Point ของคุณเรื่อง Dynamic vs Static แล้ว แต่ Sorbet ล่ะ? จะ Deprecated ไหม?

Absolutely not ความแตกต่าง Primary (และอาจสำคัญที่สุด) ระหว่าง RBS และ Sorbet คือ Former เป็นแค่ Language ในขณะที่ Latter เป็น Complete Type Checker เอง

ทีม Ruby Assert ว่า Main Goal ของ RBS คือ Describe the Structure ของโค้ดของคุณ มันจะไม่ Perform Type Checking แต่จะ Define Structure ที่ Type Checkers (เหมือน Sorbet หรือตัวอื่นๆ) สามารถใช้เพื่อ Type Check ได้ Code Structure ถูก Store ภายใน File Extension ใหม่ — .rbs

เพื่อ Check it out มา取 Ruby Class ต่อไปนี้เป็น Example:

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

มัน Represent Simple Inheritance ใน Ruby สิ่งน่าสนใจที่นี่คือเราไม่สามารถ Guess Types ของแต่ละ Attribute ที่ใช้ใน Class ได้ ยกเว้น flag

เนื่องจาก flag มาพร้อม Default Value ทั้ง Developer และ Type Checker สามารถ Infer Type เพื่อ Prevent Further Misuse ได้

สิ่งต่อไปนี้จะเป็น Proper Representation ของ Class ข้างต้นใน RBS Format:

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

ใช้เวลาย่อยๆ เพื่อ Digest นี่ มันคือ Declaration Language ดังนั้น Signatures เท่านั้นที่อาจปรากฏใน RBS File Simple มั้ย?

ไม่ว่าจะ Auto-generated โดย CLI Tool (มากกว่านี้) หรือโดยคุณ มัน Safer ที่จะ Annotate Type เป็น untyped เมื่อไม่สามารถ Guess ได้

หากคุณ Sure เกี่ยวกับ Type ของ val ตัวอย่าง RBS Mapping ของคุณอาจ Switch เป็น:

class Super attr_reader val : Integer def initialize : (val: Integer) -> void end

ยังสำคัญที่จะทราบว่า ทั้ง Ruby และ Sorbet Teams กำลัง Working (และยังคง) ทำให้เกิดและปรับปรุง RBS ประสบการณ์ของ Sorbet Team กับ Type Checking หลายปี ช่วยทีม Ruby Fine-tune หลายอย่างกับ Project นี้

Interoperability ระหว่าง RBS และ RBI Files ยังอยู่ใน Development เป้าหมายคือ Sorbet และ Checker Tools อื่นๆ จะมี Basis อย่างเป็นทางการและ Centralized ที่จะ Follow

The RBS CLI Tool

One Important Consideration ที่ทีม Ruby มีเมื่อ Develop RBS คือ Ship CLI Tool ที่ช่วย Developers ลองและเรียนรู้วิธีใช้ มันเรียกว่า rbs และมาพร้อม Default กับ Ruby 3 หากคุณยังไม่ Upgrade Ruby Version คุณสามารถ Add Gem โดยตรงไป Project ของคุณได้:

➜ gem install rbs 

Command rbs help จะแสดง Command Usage พร้อม Available Commands

List of available commands

Commands ส่วนใหญ่ Focus บน Parsing และ Analyzing Ruby Code Structures ตัวอย่าง Command ancestors Sweep Hierarchical Structure ของ Class ที่กำหนดเพื่อ Check Ancestors:

➜ rbs ancestors ::String ::String ::Comparable ::Object ::Kernel ::BasicObject

Command methods แสดง Method Structures ทั้งหมดของ Class ที่กำหนด:

➜ rbs methods ::String ! (public) != (public) !~ (public) ... Array (private) Complex (private) Float (private) ... autoload? (private) b (public) between? (public) ...

อยากดู Specific Method Structure? ไปที่ 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 วันนี้ Command prototype ช่วยมากกับ Scaffolding Types สำหรับ Classes ที่มีอยู่แล้ว Command Generate Prototypes ของ RBS Files

มา取 Test < Super Inheritance Example ก่อนหน้าและ Save Code ลง File ชื่อ appsignal.rb จากนั้น Run Command ต่อไปนี้:

➜ rbs prototype rb appsignal.rb 

เนื่องจาก Command อนุญาต Generators rb, rbi, และ runtime คุณต้อง Provide Specific Type ของ File ที่คุณ Scaffolding ทันทีหลัง Command prototype ตามด้วย File Pathname

สิ่งต่อไปนี้คือ Result ของ Execution:

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

Pretty Similar กับ RBS Version แรกของเรา ตามที่กล่าวไปก่อนหน้านี้ Tool มาร์กเป็น untyped Type ใดๆ ที่ Guess ไม่ได้

มันยัง Count สำหรับ Method Returns สังเกต Return Type ของ flag Definition ในฐานะ Developer คุณน่าจะ Sure ว่า Method เสมอ Return Boolean แต่เนื่องจาก Dynamic Nature ของ Ruby Tool ไม่สามารถ 100% บอกได้ว่าเป็นอย่างนั้น

และนั่นคือที่ที่ Ruby 3 Child อีกตัวมาช่วย: TypeProf

The TypeProf Tool

TypeProf เป็น Type Analysis Tool สำหรับ Ruby ที่สร้างบน Syntax Tree Interpretation บางอย่าง

แม้ยัง Experimental แต่มันพิสูจน์ว่า Powerful มากเมื่อมันมาถึงการเข้าใจว่าโค้ดของคุณพยายามทำอะไร

หากคุณไม่มี Ruby 3 เพียงแค่ Add Gem ไป Project:

➜ gem install typeprof 

ตอนนี้ มา Run appsignal.rb เดียวกันกับมัน:

➜ typeprof appsignal.rb 

นี่คือ Output:

# 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 ถูก Map ตอนนี้ แตกต่างจาก RBS Prototype TypeProf Scan Method Body พยายามเข้าใจว่า Actions อะไรถูก Perform บน Variable นั้น เนื่องจากมันไม่สามารถ Identify Direct Change ใดๆ ให้ Variable นี้ TypeProf Safely Mapped Method Return เป็น Boolean

พิจารณา ตัวอย่าง TypeProf จะมี Access ถึง Classes อื่นๆ ที่ Instantiate และ Use Class Test ด้วยนั้น มันสามารถไปลึกขึ้นไปใน Code ของคุณและ Fine-tune Predictions ได้ สมมติว่า Code Snippet ต่อไปนี้ถูกเพิ่มท้าย appsignal.rb:

testSub = Test.new("My value", "My value" == "") testSup = Super.new("My value")

และคุณเปลี่ยน initialize Method Signature เป็น:

def initialize(val, flag)

เมื่อ Re-run Command นี้ควรเป็น Output:

# 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

Super Cool!

TypeProf ไม่ Deal กับ Inherited Attributes ได้ดี นั่นคือเหตุผลที่เรา Instantiate Super Object ใหม่ มิฉะนั้น มันจะไม่ได้ว่า val เป็น String

Major Pro ของ TypeProf คือ Safety ทุกครั้งที่มัน Unable to Figure Something Out แน่นอน untyped จะถูก Return

Partial RBS Specification

One Important Warning จาก Official Docs ระบุว่า แม้ TypeProf จะ Powerful คุณควร Aware ของ Limitations เกี่ยวกับสิ่งที่มันสามารถและไม่สามารถ Generate ในแง่ของ RBS Code

ตัวอย่าง Common Practice ระหว่าง Ruby Developers คือ Method Overloading ซึ่งคุณ Invoke พฤติกรรมต่างๆ ของ Method ขึ้นอยู่กับ Arguments

พิจารณา Method ใหม่ spell ถูกเพิ่มเข้า Super Class ซึ่ง Return Integer หรือ String ขึ้นอยู่กับ Parameter Type:

def spell(val) if val.is_a?(String) "" else 0 end end

RBS Embraces Practice นี้โดย Allow คุณ Deal กับ Overloading ผ่าน Union Type (Value ที่ Represent Multiple Possible Types) Syntax:

def spell: (String) -> String | (Integer) -> Integer

TypeProf ไม่สามารถ Infer นี้ได้แค่จากการ Analyze Method Body เพื่อช่วยมัน คุณสามารถ Add Definition แบบนี้ลง RBS File แบบ Manual และ TypeProf จะ Check ที่นั่นก่อนเสมอสำหรับ Instructions

สำหรับนี้ คุณต้อง Add RBS File Path ท้าย Command:

typeprof appsignal.rb appsignal.rbs

ดู Output ใหม่ด้านล่าง:

class Super ... def spell: (untyped val) -> (Integer | String) end

นอกจากนี้ เรายังสามารถ Verify Real Types ระหว่าง Runtime ผ่าน Kernel#p เพื่อ Test หาก Overloading ทำงาน โดย Add 2 บรรทัดถัดไปท้าย appsignal.rb:

p testSup.spell(42) p testSup.spell("str")

นี่ควรเป็น Output:

# Revealed types # appsignal.rb:11 #=> Integer # appsignal.rb:12 #=> String ...

Make sure to Refer Official Docs สำหรับข้อมูลเพิ่มเติม โดยเฉพาะ Section เกี่ยวกับ TypeProf Limitations

Duck Typing

คุณเคยได้ยินมาก่อน หาก Ruby Object ทำทุกอย่างที่ Duck ทำ มันก็คือ Duck

ตามที่เราเห็น Ruby ไม่ Care เกี่ยวกับว่ Objects ของคุณ Meant to be อะไร Types สามารถ Change Dynamically ได้เช่นกันเช่น Object References

แม้ Helpful Duck Typing สามารถ Tricky ได้ มาดู Example

สมมติว่า ตั้งแต่ตอนนี้ Attribute val ที่คุณ Declare สำหรับ Super Class ซึ่งเป็น String ต้อง Always Convertible เป็น Integer

แทนที่จะ Trust ว่า Developers จะ Always Guarantee Conversion (อาจ Throw Error มิฉะนั้น) คุณสามารถสร้าง Interface ระบุว่า:

interface _IntegerConvertible def to_int: () -> Integer end

Interface Types Provide One หรือ More Methods ที่ Detached จาก Concrete Classes และ Modules วิธีนี้ เมื่อคุณต้องการ Type ใดๆ ถูก Pass ไป Super Instantiation คุณสามารถทำได้ง่ายๆ:

class Super attr_reader val : _IntegerConvertible def initialize : (val: _IntegerConvertible) -> void end

Concrete Class หรือ Module ที่ Implement Interface นี้จะต้อง Ensure ว่า Proper Validation ถูกทำ

Metaprogramming

Perhaps หนึ่งใน Most Dynamic Features ของ Ruby คือ Capability สร้าง Code ที่สร้าง Code เองระหว่าง Runtime นั่นคือ Metaprogramming

เนื่องจาก Uncertain Nature ของสิ่งต่างๆ RBS CLI Tool ไม่สามารถ Generate RBS ออกจาก Metaprogramming Code ได้

มา取 Snippet ต่อไปนี้เป็น Example:

class Test define_method :multiply do |*args| args.inject(1, :*) end end p Test.new.multiply(2, 3, 5)

Class นี้ Define Method ชื่อ multiply ที่ Runtime และ Instruct ให้ Inject Arguments และ Multiply แต่ละตัวกับ Previous Result

เมื่อคุณ Run RBS prototype Command นี่ควรเป็น Output:

class Test end

ขึ้นอยู่กับ Complexity ของ Metaprogramming Code ของคุณ TypeProf จะยังพยายาม Best เพื่อ Extract อะไรสักอย่างจากมัน แต่ไม่ Always Guaranteed

จำไว้ คุณสามารถ Add Type Mappings ของคุณเองเข้า RBS File และ TypeProf จะ Obey พวกเขา In Advance นั่น Valid สำหรับ Metaprogramming ด้วย

ยังสำคัญที่จะ Keep Updated กับ Latest Repository Changes เนื่องจาก Team Constantly Releasing New Features ซึ่งอาจรวม Updates บน Metaprogramming ด้วย

That being said หาก Codebase ของคุณ Include Metaprogramming บางประเภท ให้ระมัดระวังกับ Tools เหล่านี้ อย่าใช้ Blindly!

Wrapping Up

มีรายละเอียดเพียบมากเกี่ยวกับสิ่งที่เรา Discuss จนถึงตอนนี้ รวมถึง Edge Use Cases สำหรับ RBS และ TypeProf ทั้งคู่ ที่คุณควร Aware ของ

So ทำให้แน่ใจว่า Refer Official Docs สำหรับเพิ่มเติมเรื่องนั้น

RBS ยัง Fresh มาก แต่ก็ Cause Huge Impact แล้วบน Rubyists ที่เคย Type Check Codebases ของพวกเขาด้วย Tools อื่นๆ

คุณล่ะ? ลองแล้วไหม? ความคิดเห็นของคุณเกี่ยวกับ RBS คืออะไร?

P.S. หากคุณต้องการอ่านโพสต์ Ruby Magic ทันทีที่เผยแพร่ สมัครรับข่าวสาร Ruby Magic newsletter และจะไม่พลาดโพสต์เด็ดๆ ไปเลย!