RBS là tên của một ngôn ngữ định dạng cú pháp kiểu mới cho Ruby. RBS cho phép bạn thêm chú thích kiểu vào mã Ruby của mình trong các tệp có phần mở rộng mới là .rbs. Chúng trông như sau:
class MyClass
def my_method : (my_param: String) -> String
end
Bằng cách cung cấp chú thích kiểu với RBS, bạn sẽ nhận được các lợi ích sau:
- Một cách sạch sẽ và súc tích để định nghĩa cấu trúc của codebase.
- Một cách an toàn hơn để thêm kiểu vào mã kế thừa (legacy code) thông qua các tệp thay vì thay đổi trực tiếp các lớp.
- Khả năng tích hợp chung với các trình kiểm tra kiểu tĩnh và động.
- Các tính năng mới để xử lý nạp chồng phương thức, duck typing, giao diện động và nhiều hơn nữa.
Nhưng đừng vội! Có lẽ đã có các trình kiểm tra kiểu tĩnh như Sorbet và Steep rồi?
Có, và chúng rất tuyệt vời! Tuy nhiên, sau bốn năm thảo luận và một vài trình kiểm tra kiểu do cộng đồng xây dựng, nhóm committer của Ruby nghĩ rằng đã đến lúc định nghĩa một số tiêu chuẩn để xây dựng các công cụ kiểm tra kiểu.
RBS chính thức là một ngôn ngữ, và nó ra đời cùng với Ruby 3.
Ngoài ra, do bản chất kiểu động của Ruby, cũng như các mẫu phổ biến như Duck Typing và nạp chồng phương thức, một số biện pháp phòng ngừa đang được áp dụng để đảm bảo cách tiếp cận tốt nhất, chúng ta sẽ xem chi tiết ngay sau đây.
Cài Đặt Ruby 3
Để làm theo các ví dụ ở đây, chúng ta cần cài đặt Ruby 3.
Bạn có thể làm theo hướng dẫn chính thức hoặc sử dụng ruby-build trong trường hợp bạn cần quản lý nhiều phiên bản Ruby.
Thay vào đó, bạn cũng có thể cài đặt trực tiếp gem rbs vào dự án hiện tại:
gem install rbs
Kiểu Tĩnh vs Kiểu Động
Trước khi đi sâu hơn, hãy làm rõ khái niệm này. Ngôn ngữ kiểu động so sánh với kiểu tĩnh như thế nào?
Trong các ngôn ngữ kiểu động như Ruby và JavaScript, không có kiểu dữ liệu được xác định trước để trình thông dịch hiểu cách xử lý khi một phép toán bị cấm xảy ra trong thời gian chạy (runtime).
Đây là điều ngược lại với kiểu tĩnh. Các ngôn ngữ kiểu tĩnh như Java và C kiểm tra kiểu trong thời gian biên dịch (compile time).
Hãy xem đoạn mã Java sau làm tham chiếu:
int number = 0;
number = "Hi, number!";
Điều này không thể thực hiện trong kiểu tĩnh, vì dòng thứ hai sẽ ném ra lỗi:
error: incompatible types: String cannot be converted to int
Bây giờ, hãy xem cùng ví dụ trong Ruby:
number = 0
number = "Hi, number!"
puts number # In ra thành công "Hi, number!"
Trong Ruby, kiểu của một biến thay đổi ngay lập tức, nghĩa là trình thông dịch biết cách hoán đổi động từ kiểu này sang kiểu khác.
Khái niệm này thường bị nhầm lẫn với ngôn ngữ kiểu mạnh (strongly typed) vs kiểu yếu (weakly typed).
Ruby không chỉ là ngôn ngữ kiểu động mà còn là ngôn ngữ kiểu mạnh, nghĩa là nó cho phép biến thay đổi kiểu trong thời gian chạy. Tuy nhiên, nó không cho phép bạn thực hiện các phép toán trộn kiểu "điên rồ".
Hãy xem ví dụ tiếp theo (trong Ruby) được chuyển đổi từ ví dụ trước:
number = 2
sum = "2" + 2
puts sum
Lần này chúng ta thử cộng hai số thuộc hai kiểu khác nhau (một Integer và một String). Ruby sẽ ném ra lỗi sau:
main.rb:2:in `+': no implicit conversion of Integer into String (TypeError)
from main.rb:2:in `'
Nói cách khác, Ruby nói rằng việc thực hiện các phép tính phức tạp liên quan đến các kiểu có thể khác nhau hoàn toàn là trách nhiệm của bạn.
JavaScript, mặt khác, là ngôn ngữ vừa kiểu yếu vừa kiểu động, cho phép cùng mã đó với kết quả khác:
> number = 2
> sum = "2" + 2
> console.log(sum)
> 22 // nó nối chuỗi cả hai giá trị
RBS Khác Với Sorbet Như Thế Nào?
Trước tiên, từ cách tiếp cận của mỗi công cụ đối với việc chú thích mã. Trong khi Sorbet hoạt động bằng cách thêm chú thích rõ ràng khắp nơi trong mã của bạn, RBS chỉ yêu cầu tạo một tệp mới với phần mở rộng .rbs.
Lợi ích chính của cách này là khi nghĩ về việc di chuyển các codebase kế thừa. Vì các tệp gốc của bạn không bị ảnh hưởng, việc áp dụng các tệp RBS vào dự án an toàn hơn nhiều.
Theo người tạo ra, mục tiêu chính của RBS là mô tả cấu trúc của các chương trình Ruby. Nó tập trung vào việc định nghĩa chữ ký lớp/phương thức.
RBS bản thân không thể kiểm tra kiểu. Mục tiêu của nó bị giới hạn ở việc định nghĩa cấu trúc làm cơ sở cho các trình kiểm tra kiểu (như Sorbet và Steep) thực hiện công việc của chúng.
Hãy xem ví dụ về một kế thừa Ruby đơn giản:
class Badger
def initialize(brand)
@brand = brand
end
def brand?
@brand
end
end
class Honey < Badger
def initialize(brand: "Honeybadger", sweet: true)
super(brand)
@sweet = sweet
end
def sweet?
@sweet
end
end
Tuyệt vời! Chỉ có hai lớp với một vài thuộc tính và kiểu được suy luận.
Dưới đây là một biểu diễn RBS có thể:
class Brand
attr_reader brand : String
def initialize : (brand: String) -> void
end
class Honey < Brand
@sweet : bool
def initialize : (brand: String, ?sweet: bool) -> void
def sweet? : () -> bool
end
Khá giống nhau, phải không? Sự khác biệt lớn ở đây là các kiểu. Ví dụ, phương thức initialize của lớp Honey nhận một tham số String và một tham số boolean và không trả về gì.
Trong khi đó, đội ngũ Sorbet đang làm việc chặt chẽ để tạo công cụ cho phép tương tác giữa RBI (phần mở rộng mặc định của Sorbet để định nghĩa kiểu) và RBS.
Mục tiêu là đặt nền móng để Sorbet và bất kỳ trình kiểm tra kiểu nào hiểu cách sử dụng các tệp định nghĩa kiểu của RBS.
Công Cụ Tạo Khung (Scaffolding)
Nếu bạn mới bắt đầu với ngôn ngữ này và đã có một số dự án đang chạy, có thể khó để đoán nơi và cách bắt đầu thêm kiểu.
Để giải quyết điều này, nhóm Ruby đã cung cấp một công cụ CLI rất hữu ích tên là rbs để tạo khung kiểu cho các lớp hiện có.
Để liệt kê các lệnh có sẵn, chỉ cần gõ rbs help trong bảng điều khiển và kiểm tra kết quả:
Các lệnh có sẵn trong công cụ CLI.
Có lẽ lệnh quan trọng nhất trong danh sách là prototype vì nó phân tích AST của tệp mã nguồn được cung cấp làm tham số để tạo mã RBS "xấp xỉ".
Xấp xỉ vì nó không 100% chính xác. Vì mã kế thừa của bạn chủ yếu không có kiểu, hầu hết nội dung được tạo khung sẽ giống nhau. RBS không thể đoán được một số kiểu nếu không có gán rõ ràng, ví dụ.
Hãy lấy ví dụ khác làm tham chiếu, lần này liên quan đến ba lớp khác nhau trong kế thừa cấp độ:
class Animal
def initialize(weight)
@weight = weight
end
def breathe
puts "Inhale/Exhale"
end
end
class Mammal < Animal
def initialize(weight, is_terrestrial)
super(weight)
@is_terrestrial = is_terrestrial
end
def nurse
puts "I'm breastfeeding"
end
end
class Cat < Mammal
def initialize(weight, n_of_lives, is_terrestrial: true)
super(weight, is_terrestrial)
@n_of_lives = n_of_lives
end
def speak
puts "Meow"
end
end
Chỉ là các lớp đơn giản với thuộc tính và phương thức. Lưu ý rằng một trong số chúng được cung cấp với giá trị boolean mặc định, điều này sẽ quan trọng để minh họa khả năng tự đoán của RBS.
Bây giờ, để tạo khung các kiểu này, hãy chạy lệnh sau:
rbs prototype rb animal.rb mammal.rb cat.rb
Bạn có thể truyền bao nhiêu tệp Ruby cũng được. Dưới đây là kết quả của việc thực thi:
class Animal
def initialize: (untyped weight) -> untyped
def breathe: () -> untyped
end
class Mammal < Animal
def initialize: (untyped weight, untyped is_terrestrial) -> untyped
def nurse: () -> untyped
end
class Cat < Mammal
def initialize: (untyped weight, untyped n_of_lives, ?is_terrestrial: bool is_terrestrial) -> untyped
def speak: () -> untyped
end
Như chúng ta đã dự đoán, RBS không thể hiểu được hầu hết các kiểu chúng ta nhắm tới khi tạo các lớp.
Phần lớn công việc của bạn sẽ là thủ công thay đổi các tham chiếu untyped thành kiểu thực sự. Một số thảo luận nhằm tìm cách tốt hơn để thực hiện việc này đang diễn ra trong cộng đồng.
Metaprogramming (Lập Trình Siêu)
Khi nói đến metaprogramming, công cụ rbs sẽ không giúp ích nhiều do bản chất động của nó.
Hãy lấy lớp sau làm ví dụ:
class Meta
define_method :greeting, -> { puts 'Hi there!' }
end
Meta.new.greeting
Kết quả của việc tạo khung kiểu cho lớp này sẽ là:
class Meta
end
Duck Typing
Ruby không quá quan tâm đến bản chất của đối tượng (kiểu của chúng) mà quan tâm đến những gì chúng có thể làm (hành vi của chúng).
Duck typing là một phong cách lập trình nổi tiếng hoạt động theo모토:
"Nếu một đối tượng hành động như một con vịt (kêu, đi, bay, v.v.), thì nó là một con vịt"
Nói cách khác, Ruby sẽ luôn đối xử với nó như một con vịt ngay cả khi định nghĩa và kiểu ban đầu của nó không được thiết kế để đại diện cho một con vịt.
Tuy nhiên, duck typing có thể che giấu chi tiết triển khai mã mà có thể dễ dàng trở nên rắc rối và khó tìm/đọc.
RBS giới thiệu khái niệm kiểu giao diện (interface types), là một tập hợp các phương thức không phụ thuộc vào bất kỳ lớp hoặc module cụ thể nào.
Hãy lấy ví dụ kế thừa động vật trước đó và giả sử chúng ta đang thêm một cấp độ phân cấp mới cho động vật trên cạn mà Cat của chúng ta sẽ kế thừa:
class Terrestrial < Animal
def initialize(weight)
super(weight)
end
def run
puts "Running..."
end
end
Để tránh các đối tượng con không phải động vật trên cạn chạy, chúng ta có thể tạo một giao diện kiểm tra kiểu cụ thể cho hành động đó:
interface _CanRun
# Yêu cầu toán tử `<<` chấp nhận đối tượng `Terrestrial`.
def <<: (Terrestrial) -> void
end
Khi ánh xạ mã RBS vào phương thức run cụ thể, chữ ký sẽ là:
def run: (_CanRun) -> void
Bất cứ khi nào ai đó cố gắng truyền bất cứ thứ gì khác ngoài đối tượng Terrestrial vào phương thức, trình kiểm tra kiểu sẽ đảm bảo ghi lại lỗi.
Kiểu Union (Union Types)
Cũng phổ biến trong cộng đồng Ruby là việc có các biểu thức chứa nhiều kiểu giá trị khác nhau.
def fly: () -> (Mammal | Bird | Insect)
RBS hỗ trợ kiểu union bằng cách đơn giản nối chúng qua toán tử pipe.
Nạp Chồng Phương Thức (Method Overloading)
Một thực hành phổ biến khác (trong nhiều ngôn ngữ lập trình) là cho phép nạp chồng phương thức, trong đó một lớp có thể có nhiều phương thức cùng tên nhưng chữ ký khác nhau (như kiểu hoặc số lượng tham số, thứ tự của chúng, v.v.).
Hãy lấy ví dụ một động vật có thể trả về các cháu tiến hóa gần nhất của nó:
def evolutionary_cousins: () -> Enumerator[Animal, void] | { (Animal) -> void } -> void
Bằng cách này, RBS cho phép chúng ta xác định rõ ràng liệu một động vật nhất định sẽ có một cháu tiến hóa đơn lẻ hay một đám chúng.
TypeProf
Song song đó, nhóm Ruby cũng đã bắt đầu một dự án mới tên là typeprof, một trình thông dịch Ruby cấp độ kiểu thử nghiệm nhằm phân tích và (cố gắng) tạo nội dung RBS.
Nó hoạt động bằng cách diễn giải trừu tượng (abstract interpretation) và vẫn đang ở những bước đầu tiên để hoàn thiện hơn, vì vậy hãy cẩn thận khi sử dụng cho mục đích sản xuất.
Để cài đặt, chỉ cần thêm gem vào dự án:
gem install typeprof
Lưu ý rằng nó yêu cầu phiên bản Ruby lớn hơn 2.7.
Hãy lấy phiên bản sau của lớp Animal:
class Animal
def initialize(weight)
@weight = weight
end
def die(age)
if age > 50
true
elsif age <= 50
false
elsif age < 0
nil
end
end
end
Animal.new(100).die(65)
Dựa trên những gì đang diễn ra bên trong phương thức age và lời gọi tiếp theo đến cùng phương thức, TypeProf có thể suy luận thông minh các kiểu được thao tác trong mã.
Khi bạn chạy lệnh typeprof animal.rb, đầu ra sẽ như sau:
## Classes
class Animal
@weight: Integer
def initialize: (Integer weight) -> Integer
def die: (Integer age) -> bool?
end
Đây là một công cụ mạnh mẽ có nhiều thứ để cung cấp cho các dự án đã có nhiều mã.
Tích Hợp VS Code
Hiện tại, không có nhiều plugin VS Code có sẵn để xử lý RBS cho định dạng, kiểm tra cấu trúc, v.v. Đặc biệt vì nó còn khá mới.
Tuy nhiên, nếu bạn tìm kiếm "RBS" trong cửa hàng, bạn có thể tìm thấy một plugin tên ruby-signature sẽ giúp nổi bật cú pháp, như hình dưới:
Nổi bật cú pháp RBS trong VS Code.
Kết Luận
RBS còn rất mới nhưng đã đại diện cho một bước quan trọng về phía codebase Ruby an toàn hơn.
Thông thường, theo thời gian, các công cụ mới và dự án mã nguồn mở sẽ nổi lên để hỗ trợ nó, chẳng hạn như RBS Rails để tạo tệp RBS cho các ứng dụng Ruby on Rails.
Tương lai giữ những điều tuyệt vời cho cộng đồng Ruby với các ứng dụng an toàn và ít lỗi hơn. Không thể chờ đợi để thấy nó!