Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Giới thiệu RBS: Ngôn ngữ Typing Mới của Ruby 3 cho Code Đáng tin cậy Hơn

Phiên bản 3.0.0 của Ruby đã được release sau nhiều lần chờ đợi. Cùng với nhiều cải tiến tuyệt vời, như performance boost 3x nhanh hơn so với phiên bản trước, tính năng experimental concurrency-parallel, v.v., Ruby team cũng giới thiệu một syntax language mới cho dynamic typing trong Ruby: RBS.

Đây là thứ team đã thảo luận nhiều năm, dựa trên thành công của community-developed tools cho static type checking như Sorbet.

Sorbet là powerful type checker backed by Stripe. Nó check code bằng cả annotate và/hoặc định nghĩa RBI files. RBI files, lần lượt, hoạt động như interface giữa static và dynamic component cung cấp "mô tả" của chúng (constants, ancestors, metaprogramming code, và hơn).

Vậy, nếu Sorbet chủ yếu deal với static checking và RBS được làm để address dynamic typing, khác biệt giữa chúng là gì? Làm sao cả hai cùng coexist? Khi nào nên dùng cái này thay vì cái kia?

Đó là những câu hỏi khá phổ biến về vai trò chính của RBS. Đó là lý do chúng tôi quyết định viết bài này. Để làm rõ, thực tế, tại sao bạn nên cân nhắc adopt nó dựa trên khả năng của nó. Hãy đi sâu ngay!

Bắt đầu với Cơ bản

Hãy bắt đầu với sự hiểu rõ sự khác biệt giữa static typing và dynamic typing. Mặc dù basic, đây là key concept để nắm bắt vai trò của RBS.

Hãy lấy code snippet từ statically typed language làm reference:

➜ String str = ""; str = 2.4;

Không tin tức gì rằng ngôn ngữ như vậy quan tâm type của object và variable. Có nói vậy, code như trên sẽ throw error.

Ruby, giống nhiều ngôn ngữ khác như JavaScript, Python, và Objective-C, không chú trọng lắm đến type bạn target cho object.

Cùng code trong Ruby sẽ chạy thành công, như dưới:

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

Điều này possible vì Ruby interpreter biết cách dynamically switch từ type này sang type khác.

Tuy nhiên, có limit những gì interpreter cho phép. Ví dụ, lấy thay đổi code sau:

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

Này sẽ produce error sau:

Error: no implicit conversion of Float into String

Chạy cùng code với JavaScript, ví dụ, sẽ chạy fine.

Bài học: Ruby infer type động, đúng; nhưng, khác với dynamic language chính khác, nó không accept mọi thứ. Hãy nhận thức điều đó!

Và đó là nơi type checker (dù static hay dynamic) trở nên hữu ích.

RBS vs Sorbet

Đúng, tôi hiểu điểm của bạn về dynamic vs static. Nhưng, Sorbet sao? Nó sẽ bị deprecated?

Hoàn toàn không. Khác biệt chính (và có lẽ quan trọng nhất) giữa RBS và Sorbet là RBS chỉ là một language, trong khi Sorbet là complete type checker bản thân.

Ruby team khẳng định mục tiêu chính của RBS là mô tả cấu trúc code của bạn. Nó không perform type checking, mà define structure mà type checkers (như Sorbet hoặc bất kỳ cái nào khác) có thể dùng để, well, type check. Cấu trúc code được lưu trong file extension mới — .rbs.

Để check, hãy lấy Ruby class sau làm ví dụ:

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

Nó represent một simple inheritance trong Ruby. Điều thú vị ở đây là chúng ta không thể guess type của từng attribute dùng trong class, trừ flag.

Vì flag được initialized với default value, cả developer và type checker có thể infer type để ngăn misuse thêm.

Sau đây sẽ là representation đúng của class trên ở 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

Hãy dành thời gian tiêu hóa. Đây là declaration language, nên chỉ signature có thể xuất hiện trong RBS file. Đơn giản, phải không?

Dù được autogenerate bởi CLI tool (sẽ nói sau) hay bởi bạn, an toàn hơn để annotate type là untyped khi không guess được.

Nếu bạn chắc về type của val, ví dụ, RBS mapping của bạn có thể switch sang:

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

Cũng quan trọng note rằng cả Ruby và Sorbet team đều đang làm việc (vẫn đang) cho creation và improvement của RBS. Experience của Sorbet team với type checking, nhiều năm, đã giúp Ruby team fine-tune nhiều thứ với project này.

Interoperability giữa RBS và RBI files vẫn đang development. Mục tiêu là Sorbet và checker tools khác có official và centralized basis để follow.

RBS CLI Tool

Một 고려 quan trọng Ruby team có khi develop RBS là ship CLI tool giúp developer thử và học cách dùng. Nó gọi là rbs và mặc định có trong Ruby 3. Nếu bạn chưa upgrade Ruby version, bạn có thể add gem trực tiếp vào project:

➜ gem install rbs 

Command rbs help sẽ show command usage cùng available commands.

List of available commands

Hầu hết command focus trên parsing và analyzing Ruby code structure. Ví dụ, command ancestors quét hierarchical structure của class cho trước để check ancestors:

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

Command methods hiển thị tất cả method structure của class cho trước:

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

Muốn xem specific method structure? Dùng 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

Cho những ai bắt đầu RBS hôm nay, command prototype giúp rất nhiều scaffolding type cho class đã tồn tại. Command generate prototypes của RBS files.

Hãy lấy ví dụ Test < Super inheritance trước và save code vào file appsignal.rb. Sau đó chạy command:

➜ rbs prototype rb appsignal.rb 

Vì command allow rb, rbi, và runtime generators, bạn cần provide specific type file bạn scaffolding ngay sau command prototype, theo sau file pathname.

Sau đây là kết quả 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 đến RBS version đầu tiên. Như đã nói, tool mark untyped bất kỳ type nào không guess được.

Nó cũng count cho method returns. Chú ý return type của flag definition. Là developer, bạn chắc chắn method luôn return boolean, nhưng do dynamic nature của Ruby, tool không thể 100% nói nó là boolean.

Và đó là lúc một Ruby 3 child khác đến cứu: TypeProf.

TypeProf Tool

TypeProf là type analysis tool cho Ruby được tạo trên syntax tree interpretation.

Dù vẫn experimental, nó đã chứng minh rất powerful khi nói đến việc hiểu code bạn đang cố làm gì.

Nếu chưa có Ruby 3, chỉ cần add gem vào project:

➜ gem install typeprof 

Bây giờ, chạy cùng appsignal.rb file against nó:

➜ typeprof appsignal.rb 

Đây là 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

Chú ý flag được mapped như thế nào. Điều này chỉ possible vì, khác với RBS prototype, TypeProf scan method body cố gắng hiểu action được perform trên variable cụ thể. Vì không identify bất kỳ direct change nào cho variable này, TypeProf safely map method return as boolean.

Xét ví dụ, TypeProf có access đến class khác instantiate và dùng Test class. Với đó, nó có thể đi sâu hơn vào code và fine-tune prediction. Giả sử code snippet sau thêm ở cuối appsignal.rb:

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

Và bạn thay đổi initialize method signature thành:

def initialize(val, flag)

Khi re-run command, output nên là:

# 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 không deal tốt với inherited attribute. Đó là lý do chúng ta instantiate mới Super object. Nếu không, nó không get được val là String.

Major pro của TypeProf là safety. Bất cứ khi nào nó unable figure out chắc chắn, untyped sẽ được return.

Partial RBS Specification

Một cảnh báo quan trọng từ official docs: dù TypeProf rất powerful, bạn nên aware limitation của nó về gì nó có thể và không thể generate về RBS code.

Ví dụ, common practice trong Ruby developer là method overloading mà bạn invoke behavior khác của method tùy argument.

Xét method mới spell thêm vào Super class, return Integer hoặc String dựa trên parameter type:

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

RBS embrace practice này bằng cách allow bạn deal với overloading qua union type (giá trị represent multiple possible types) syntax:

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

TypeProf không infer được điều này chỉ bằng cách analyze method body. Để giúp nó, bạn có thể manually add definition như vậy vào RBS file và TypeProf sẽ luôn check đó first cho instruction.

Cho việc này, bạn phải add RBS file path ở cuối command:

typeprof appsignal.rb appsignal.rbs

Xem output mới:

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

Thêm vào đó, chúng ta cũng có thể verify real types tại runtime qua Kernel#p để test overloading có chạy không bằng cách thêm 2 dòng sau vào cuối appsignal.rb:

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

Output nên là:

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

Hãy tham khảo official docs để biết thêm, đặc biệt section về TypeProf limitations.

Duck Typing

Bạn đã nghe về đó. Nếu Ruby object làm mọi thứ duck làm, thì nó là duck.

Như chúng ta đã thấy, Ruby không care object của bạn meant to be gì. Type có thể thay đổi dynamic cũng như object reference.

Mặc dù helpful, duck typing có thể tricky. Hãy xem ví dụ.

Giả sử, từ bây giờ, attribute val bạn declared cho Super class, là String, phải luôn convertible thành integer.

Thay vì trust developer luôn guarantee conversion (có thể throw error otherwise), bạn có thể tạo interface nói rằng:

interface _IntegerConvertible def to_int: () -> Integer end

Interface types cung cấp một hoặc nhiều method detached từ concrete class và module. Cách này, khi bạn muốn type cụ thể pass cho Super instantiation, bạn chỉ cần:

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

Concrete class hoặc module implement interface này sẽ phải đảm bảo validation đúng được làm.

Metaprogramming

Có lẽ một trong những tính năng dynamic nhất của Ruby là capability tạo code tự tạo code bởi chính nó tại runtime. Đó là metaprogramming.

Do uncertain nature, RBS CLI tool không thể generate RBS từ metaprogramming code.

Hãy lấy snippet sau làm ví dụ:

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

Class này define method multiply tại runtime và instruct nó inject argument và multiply từng cái với previous result.

Khi chạy RBS prototype command, output nên là:

class Test end

Tùy complexity metaprogramming code, TypeProf vẫn sẽ try best extract gì đó từ nó. Nhưng không luôn guaranteed.

Nhớ rằng, bạn luôn có thể add own type mapping vào RBS file và TypeProf sẽ obey chúng in advance. Điều đó cũng valid cho metaprogramming.

Cũng quan trọng keep updated với latest repository changes vì team liên tục release feature mới, có thể include update về metaprogramming.

Được nói vậy, nếu codebase của bạn include metaprogramming, hãy cẩn thận với tools này. Đừng dùng blindly!

Kết thúc

Còn nhiều detail hơn về thứ chúng ta đã discuss, cũng như edge use cases cho cả RBS và TypeProf bạn nên aware.

Vậy, hãy tham khảo official docs cho thêm về đó.

RBS vẫn rất fresh nhưng đã gây huge impact trên Rubyists quen type check codebase với tools khác.

Còn bạn? Bạn đã thử chưa? Suy nghĩ của bạn về RBS?

P.S. Nếu bạn muốn đọc bài Ruby Magic ngay khi ra mắt, hãy subscribe newsletter Ruby Magic và không bao giờ bỏ lỡ bài nào!