Phiên bản 3.0.0 được mong đợi từ lâu của Ruby cuối cùng đã được phát hành. Cùng với nhiều cải tiến tuyệt vời, chẳng hạn như tăng hiệu suất nhanh hơn gấp 3 lần so với phiên bản trước, các tính năng thử nghiệm đồng thời song song, v.v., nhóm Ruby cũng giới thiệu một ngôn ngữ cú pháp mới để nhập động trong Ruby:RBS.
Đó là điều mà nhóm đã thảo luận trong nhiều năm, dựa trên sự thành công của các công cụ do cộng đồng phát triển để kiểm tra kiểu tĩnh như Sorbet.
Sorbet là một công cụ kiểm tra kiểu mạnh mẽ được hỗ trợ bởi Stripe. Nó kiểm tra mã của bạn bằng cách ghi chú thích và / hoặc xác định các tệp RBI. Đến lượt nó, các tệp RBI hoạt động như giao diện giữa các thành phần tĩnh và động, cung cấp "mô tả" về chúng (hằng số, tổ tiên, mã lập trình siêu mẫu, v.v.).
Vì vậy, nếu Sorbet chủ yếu giải quyết việc kiểm tra tĩnh và RBS được tạo ra để giải quyết việc nhập động, thì sự khác biệt giữa chúng là gì? Cả hai sẽ cùng tồn tại như thế nào? Khi nào tôi nên sử 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 tại sao chúng tôi quyết định viết tác phẩm này. Để làm rõ, trên thực tế, tại sao bạn nên cân nhắc việc áp dụng nó dựa trên những gì nó có khả năng. Hãy đi sâu vào ngay!
Bắt đầu với Kiến thức cơ bản
Hãy bắt đầu với sự hiểu biết rõ ràng về sự khác biệt giữa nhập tĩnh _ và gõ động_. Mặc dù nó là cơ bản, nhưng nó là một khái niệm chính cần nắm bắt để hiểu được vai trò của RBS.
Hãy lấy một đoạn mã từ một ngôn ngữ được nhập tĩnh làm tham chiếu:
➜
String str = "";
str = 2.4;
Không có tin tức gì khi một ngôn ngữ như vậy quan tâm đến các loại đối tượng và biến của nó. Điều đó đang được nói, mã như mã ở trên sẽ gây ra lỗi.
Ruby, giống như nhiều ngôn ngữ khác như JavaScript, Python và Objective-C, không chú ý nhiều đến loại mà bạn đang nhắm mục tiêu cho các đối tượng của mình.
Đoạn mã tương tự trong Ruby sẽ chạy thành công, như dưới đây:
➜ irb
str = ""
str = 2.4
puts str # prints 2.4
Điều này có thể thực hiện được vì trình thông dịch của Ruby biết cách động chuyển từ loại này sang loại khác.
Tuy nhiên, có một giới hạn đối với những gì trình thông dịch cho phép. Ví dụ:thực hiện thay đổi mã sau:
➜ irb
val = "6.0"
result = val + 2.0
puts result
Điều này sẽ tạo ra lỗi sau:
Lỗi:không có chuyển đổi ngầm định Float thành Chuỗi
Ví dụ:chạy cùng một mã với JavaScript sẽ chạy tốt.
Đạo đức của câu chuyện:Ruby thực sự suy luận các loại động; nhưng, không giống như các ngôn ngữ động chính khác, nó sẽ không chấp nhận mọi thứ. Hãy lưu ý điều đó!
Và đó là nơi mà các bộ kiểm tra kiểu (dù tĩnh hay động) trở nên hữu ích.
RBS vs Sorbet
Đúng vậy, tôi đã hiểu ý của bạn về điều động và tĩnh. Nhưng, còn Sorbet thì sao? Nó có bị ngừng sử dụng không?
Không có gì. Sự khác biệt chính (và có lẽ là quan trọng nhất) giữa RBS và Sorbet là ngôn ngữ đầu tiên chỉ là một ngôn ngữ, trong khi ngôn ngữ thứ hai là một trình kiểm tra kiểu hoàn chỉnh.
Nhóm Ruby khẳng định mục tiêu chính của RBS là mô tả cấu trúc mã của bạn. Nó sẽ không thực hiện việc kiểm tra kiểu, mà thay vào đó, xác định cấu trúc mà người kiểm tra kiểu (như Sorbet hoặc bất kỳ loại nào khác) có thể sử dụng để kiểm tra kiểu. Cấu trúc mã được lưu trữ trong phần mở rộng tệp mới - .rbs .
Để kiểm tra, hãy lấy lớp Ruby 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ó đại diện cho một kế thừa đơn giản trong Ruby. Điều thú vị cần lưu ý ở đây là chúng ta không thể đoán các kiểu của từng thuộc tính được sử dụng trong lớp, ngoại trừ cờ flag
.
Kể từ khi flag
được khởi tạo với một giá trị mặc định, cả nhà phát triển và người kiểm tra loại đều có thể suy ra loại để ngăn việc lạm dụng thêm.
Sau đây sẽ là một đại diện thích hợp của lớp trên ở định dạng RBS:
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 một chút thời gian để tiêu hóa điều này. Đó là ngôn ngữ khai báo, vì vậy chỉ chữ ký mới có thể xuất hiện trong tệp RBS. Đơn giản phải không?
Cho dù nó được tạo tự động bởi một công cụ CLI (sẽ nói thêm về điều đó sau này) hay bởi bạn, sẽ an toàn hơn nếu chú thích một loại là untyped
khi nó không thể đoán được.
Nếu bạn chắc chắn về loại val
, ví dụ:ánh xạ RBS của bạn có thể được chuyển sang như sau:
class Super
attr_reader val : Integer
def initialize : (val: Integer) -> void
end
Cũng cần lưu ý rằng cả nhóm Ruby và Sorbet đều đang làm việc (và vẫn đang) hướng tới việc tạo ra và cải tiến RBS. Chính kinh nghiệm kiểm tra kiểu trong nhiều năm của nhóm Sorbet đã giúp nhóm Ruby tinh chỉnh rất nhiều thứ với dự án này.
Khả năng tương tác giữa các tệp RBS và RBI vẫn đang được phát triển. Mục đích là để Sorbet và bất kỳ công cụ kiểm tra nào khác có cơ sở chính thức và tập trung để tuân theo.
Công cụ RBS CLI
Một điều quan trọng mà nhóm Ruby đã cân nhắc khi phát triển RBS là đưa ra một công cụ CLI có thể giúp các nhà phát triển dùng thử và học cách sử dụng nó. Nó được gọi là rbs và đi kèm theo mặc định với Ruby 3. Nếu bạn vẫn chưa nâng cấp phiên bản Ruby của mình, bạn cũng có thể thêm đá quý của nó trực tiếp vào dự án của mình:
➜ gem install rbs
Lệnh rbs help
sẽ hiển thị cách sử dụng lệnh cùng với các lệnh có sẵn.
Danh sách các lệnh có sẵn
Hầu hết các lệnh này tập trung vào phân tích cú pháp và phân tích cấu trúc mã Ruby. Ví dụ:lệnh ancestors
quét cấu trúc phân cấp của một lớp nhất định để kiểm tra tổ tiên của nó:
➜ rbs ancestors ::String
::String
::Comparable
::Object
::Kernel
::BasicObject
Lệnh methods
hiển thị tất cả các cấu trúc phương thức của một lớp nhất định:
➜ rbs methods ::String
! (public)
!= (public)
!~ (public)
...
Array (private)
Complex (private)
Float (private)
...
autoload? (private)
b (public)
between? (public)
...
Bạn muốn xem một cấu trúc phương pháp cụ thể? Truy cập phương thức flag
:
➜ 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
Đối với những người bắt đầu với RBS ngày nay, lệnh prototype
có thể giúp ích rất nhiều cho các loại giàn giáo cho các lớp đã tồn tại. Lệnh tạo nguyên mẫu của tệp RBS.
Hãy làm bài kiểm tra Test < Super
trước ví dụ về kế thừa và lưu mã vào một tệp có tên là appsignal.rb . Sau đó, chạy lệnh sau:
➜ rbs prototype rb appsignal.rb
Vì lệnh cho phép rb , rbi và thời gian chạy trình tạo, bạn cần cung cấp loại tệp cụ thể mà bạn đang làm giàn giáo ngay sau prototype
, theo sau là tên đường dẫn tệp.
Sau đây là kết quả của việc thực hiện:
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
Khá giống với phiên bản RBS đầu tiên của chúng tôi. Như đã đề cập trước đó, công cụ đánh dấu là untyped
bất kỳ loại nào không thể đoán được.
Nó cũng tính cho các phương thức trả về. Lưu ý kiểu trả về của flag
Định nghĩa. Là một nhà phát triển, bạn có thể chắc chắn rằng phương thức này luôn trả về một boolean, nhưng do bản chất động của Ruby, công cụ này không thể chắc chắn 100% là như vậy.
Và đó là khi một đứa trẻ Ruby 3 khác đến giải cứu:TypeProf.
Công cụ TypeProf
TypeProf là một công cụ phân tích kiểu cho Ruby, được tạo trên một số diễn giải cây cú pháp.
Mặc dù vẫn đang trong giai đoạn thử nghiệm nhưng nó đã tỏ ra rất mạnh mẽ khi hiểu được mã của bạn đang cố gắng thực hiện những gì.
Nếu bạn chưa có Ruby 3, chỉ cần thêm đá quý vào dự án của bạn:
➜ gem install typeprof
Bây giờ, hãy chạy cùng một appsignal.rb chống lại nó:
➜ typeprof appsignal.rb
Đây là kết quả:
# 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
Lưu ý cách gắn cờ flag
được lập bản đồ ngay bây giờ. Điều này chỉ có thể thực hiện được bởi vì, không giống như những gì nguyên mẫu RBS làm, TypeProf quét phần thân của phương thức để cố gắng hiểu những hành động nào đang được thực hiện trên biến cụ thể đó. Vì nó không thể xác định bất kỳ thay đổi trực tiếp nào đối với biến này, TypeProf đã ánh xạ an toàn phương thức trả về dưới dạng boolean.
Ví dụ:hãy xem xét rằng TypeProf sẽ có quyền truy cập vào các lớp khác khởi tạo và sử dụng Test
lớp. Với điều đó trong tay, nó có thể đi sâu hơn vào mã của bạn và tinh chỉnh các dự đoán của nó. Giả sử rằng đoạn mã sau được thêm vào cuối appsignal.rb tệp:
testSub = Test.new("My value", "My value" == "")
testSup = Super.new("My value")
Và bạn đã thay đổi initialize
chữ ký phương thức như sau:
def initialize(val, flag)
Khi bạn chạy lại lệnh, đây sẽ là đầu ra:
# 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
Tuyệt vời!
TypeProf không thể xử lý tốt các thuộc tính được kế thừa. Đó là lý do tại sao chúng tôi tạo ra một Super
mới sự vật. Nếu không, nó sẽ không nhận được val
đó là một String
.
Ưu điểm chính của TypeProf là tính an toàn của nó. Bất cứ khi nào nó không thể tìm ra điều gì đó chắc chắn, thì hãy untyped
sẽ được trả lại.
Đặc điểm kỹ thuật RBS một phần
Một cảnh báo quan trọng từ các tài liệu chính thức nói rằng, mặc dù TypeProf rất mạnh, bạn nên biết những hạn chế của nó liên quan đến những gì nó có thể và không thể tạo ra về mã RBS.
Ví dụ:một thực tế phổ biến giữa các nhà phát triển Ruby là nạp chồng phương thức, trong đó bạn gọi các hành vi khác nhau của một phương thức tùy thuộc vào các đối số của nó.
Hãy xem xét rằng một phương pháp mới spell
được thêm vào Super
lớp, trả về một Integer
hoặc String
dựa trên loại tham số:
def spell(val)
if val.is_a?(String)
""
else
0
end
end
RBS áp dụng phương pháp này bằng cách cho phép bạn xử lý quá tải thông qua kiểu liên hợp (một giá trị đại diện cho nhiều kiểu có thể có) cú pháp:
def spell: (String) -> String | (Integer) -> Integer
TypeProf không thể suy ra điều này chỉ bằng cách phân tích phần thân của phương thức. Để giải quyết vấn đề này, bạn có thể thêm định nghĩa như vậy theo cách thủ công vào tệp RBS của mình và TypeProf sẽ luôn kiểm tra ở đó trước để biết hướng dẫn.
Đối với điều này, bạn phải thêm đường dẫn tệp RBS vào cuối lệnh:
typeprof appsignal.rb appsignal.rbs
Xem bên dưới kết quả mới:
class Super
...
def spell: (untyped val) -> (Integer | String)
end
Ngoài ra, chúng tôi cũng có thể xác minh các loại thực trong thời gian chạy thông qua Kernel#p
để kiểm tra xem quá tải có hoạt động hay không bằng cách thêm hai dòng tiếp theo vào cuối appsignal.rb tệp:
p testSup.spell(42)
p testSup.spell("str")
Đây sẽ là đầu ra:
# Revealed types
# appsignal.rb:11 #=> Integer
# appsignal.rb:12 #=> String
...
Đảm bảo tham khảo các tài liệu chính thức để biết thêm thông tin, đặc biệt là phần liên quan đến các giới hạn của TypeProf.
Vịt gõ
Bạn đã nghe nói về điều đó trước đây. Nếu một đối tượng Ruby làm mọi thứ mà một con vịt làm, thì nó là một con vịt.
Như chúng ta đã thấy, Ruby không quan tâm đến mục đích của các đối tượng của bạn. Các kiểu có thể thay đổi động cũng như các tham chiếu đối tượng.
Mặc dù hữu ích, nhưng gõ vịt có thể phức tạp. Hãy xem một ví dụ.
Giả sử rằng, từ bây giờ, val
thuộc tính bạn đã khai báo cho Super
lớp, là một String
, phải luôn có thể chuyển đổi thành số nguyên.
Thay vì tin tưởng rằng các nhà phát triển sẽ luôn đảm bảo việc chuyển đổi (có thể là sẽ xảy ra lỗi), bạn có thể tạo giao diện nói rõ rằng:
interface _IntegerConvertible
def to_int: () -> Integer
end
Các kiểu giao diện cung cấp một hoặc nhiều phương thức tách rời khỏi các lớp và mô-đun cụ thể. Bằng cách này, khi bạn muốn một loại nhất định được chuyển đến Super Instantiation, bạn có thể chỉ cần thực hiện như sau:
class Super
attr_reader val : _IntegerConvertible
def initialize : (val: _IntegerConvertible) -> void
end
Lớp hoặc mô-đun cụ thể triển khai giao diện này sẽ phải đảm bảo việc xác thực đúng cách được thực hiện.
Lập trình siêu thị
Có lẽ một trong những tính năng năng động nhất của Ruby là khả năng tạo mã tự tạo mã trong thời gian chạy. Đó là lập trình siêu hình.
Do bản chất không chắc chắn của mọi thứ, công cụ RBS CLI không thể tạo RBS từ mã lập trình siêu thị.
Hãy lấy đoạn mã sau làm ví dụ:
class Test
define_method :multiply do |*args|
args.inject(1, :*)
end
end
p Test.new.multiply(2, 3, 5)
Lớp này định nghĩa một phương thức được gọi là multiply
trong thời gian chạy và hướng dẫn nó chèn các đối số và nhân từng đối số với kết quả trước đó.
Khi bạn chạy RBS prototype
, đây sẽ là đầu ra:
class Test
end
Tùy thuộc vào độ phức tạp của mã lập trình siêu hình của bạn, TypeProf vẫn sẽ cố gắng hết sức để trích xuất một thứ gì đó từ nó. Nhưng không phải lúc nào nó cũng được đảm bảo.
Hãy nhớ rằng, bạn luôn có thể thêm ánh xạ kiểu của riêng mình vào tệp RBS và TypeProf sẽ tuân theo chúng trước. Điều đó cũng hợp lệ cho lập trình siêu hình.
Điều quan trọng là phải cập nhật những thay đổi mới nhất về kho lưu trữ vì nhóm liên tục phát hành các tính năng mới, có thể bao gồm các cập nhật về lập trình siêu thị.
Điều đó đang được nói, nếu cơ sở mã của bạn bao gồm một số kiểu lập trình siêu hình, hãy cẩn thận với những công cụ này. Đừng sử dụng chúng một cách mù quáng!
Kết thúc
Có rất nhiều chi tiết khác về những gì chúng ta đã thảo luận cho đến nay, cũng như các trường hợp sử dụng cạnh cho cả RBS và TypeProf mà bạn nên biết.
Vì vậy, hãy đảm bảo tham khảo các tài liệu chính thức để biết thêm về điều đó.
RBS vẫn còn rất mới nhưng đã gây ra tác động lớn đến các Rubyist được sử dụng để đánh máy kiểm tra cơ sở mã của họ bằng các công cụ khác.
Thế còn bạn? Bạn đã dùng thử chưa? Suy nghĩ của bạn về RBS là gì?
Tái bút. Nếu bạn muốn đọc các bài đăng của Ruby Magic ngay khi chúng xuất hiện trên báo chí, hãy đăng ký bản tin Ruby Magic của chúng tôi và không bao giờ bỏ lỡ một bài đăng nào!