Hãy bắt đầu bài đăng này với một trò chơi đoán nhỏ vui nhộn:bạn nghĩ lỗi phổ biến nhất được AppSignal theo dõi trong các ứng dụng Ruby là gì?
Thật công bằng khi giả định rằng nhiều người trong số các bạn đã trả lời câu hỏi này bằng NoMethodError
, một ngoại lệ gây ra bởi việc gọi một phương thức không tồn tại trên một đối tượng. Đôi khi, điều này có thể do lỗi đánh máy trong tên phương thức, nhưng thường thì đó là kết quả của việc gọi một phương thức trên một đối tượng không đúng loại, thường xảy ra là một nil
không mong muốn . Có điều gì chúng ta có thể làm với tư cách là nhà phát triển Ruby để giảm tần suất các lỗi như vậy không?
Các loại cần giải cứu?
Ngoại trừ việc lựa chọn trình soạn thảo văn bản hoặc ngôn ngữ lập trình, rất ít chủ đề có thể trở thành các cuộc tranh luận sôi nổi nhanh hơn các cuộc thảo luận về hệ thống kiểu. Chúng ta sẽ không có thời gian để đi vào chi tiết ở đây, nhưng bài đăng của Chris Smith "Những điều cần biết trước khi các hệ thống kiểu tranh luận" đã thực hiện rất tốt điều đó.
Theo thuật ngữ rộng nhất, hệ thống kiểu có thể được chia thành hai loại chính - tĩnh và động. Mặc dù điều trước đây xảy ra trước thời hạn (thông qua trình biên dịch hoặc một công cụ riêng biệt), kiểm tra loại động xảy ra trong thời gian chạy, nơi nó có thể dẫn đến ngoại lệ nếu các loại thực tế không phù hợp với mong đợi của nhà phát triển.
Những người ủng hộ cả hai triết lý đều có ý kiến mạnh mẽ, nhưng than ôi, cũng có nhiều quan niệm sai lầm nổi lên xung quanh:nhập tĩnh không yêu cầu chú thích kiểu phong phú — nhiều trình biên dịch hiện đại có thể tự tìm ra các loại, một quy trình được gọi là "suy luận kiểu". Mặt khác, các ngôn ngữ được nhập động dường như không có tỷ lệ sai sót cao hơn đáng kể so với các ngôn ngữ được nhập tĩnh.
Vịt gõ
Bản thân Ruby là một ngôn ngữ được kiểm tra kiểu động và tuân theo cách tiếp cận "kiểu gõ vịt":
Nếu nó đi như một con vịt và nó lang thang như một con vịt, thì nó phải là một con vịt.
Điều này có nghĩa là các nhà phát triển Ruby thường không lo lắng quá nhiều về kiểu của một đối tượng, nhưng liệu nó có phản hồi với một số "thông báo" (hoặc phương thức) nhất định hay không.
Vì vậy, tại sao phải bận tâm với việc nhập tĩnh trong Ruby, bạn có thể hỏi? Mặc dù chắc chắn không có thuốc chữa bách bệnh nào giúp mã của bạn không bị lỗi một cách kỳ diệu, nhưng nó mang lại một số lợi ích nhất định:
- Tính đúng đắn:nhập tĩnh tốt trong việc ngăn chặn một số lớp nhất định lỗi, như
NoMethodError
nói trên . - Công cụ:đôi khi, có sẵn thông tin kiểu tĩnh trong quá trình phát triển dẫn đến các tùy chọn công cụ tốt hơn (ví dụ:hỗ trợ cấu trúc lại trong IDE, v.v.)
- Tài liệu:nhiều ngôn ngữ được nhập tĩnh có các công cụ tài liệu tích hợp sẵn tuyệt vời. Haskell's Hoogle sử dụng điều này rất hiệu quả bằng cách cung cấp một công cụ tìm kiếm nơi các chức năng có thể được tra cứu bằng chữ ký loại của chúng.
- Hiệu suất:càng có nhiều thông tin cho trình biên dịch, thì càng có nhiều khả năng áp dụng tối ưu hóa hiệu suất.
Danh sách này không đầy đủ và người ta có thể tìm thấy các ví dụ phản bác cho hầu hết các điểm này, nhưng chắc chắn có một phần cốt lõi của sự thật đối với chúng.
Kiểm tra kiểu dần dần
Trong những năm gần đây, một cách tiếp cận thường được gọi là "kiểm tra kiểu dần dần" đã xâm nhập vào nhiều ngôn ngữ được kiểm tra kiểu động khác nhau:từ TypeScript cho JS đến Hack cho PHP và mypy cho Python. Điểm chung của các phương pháp này là chúng không yêu cầu phương pháp tất cả hoặc không có gì, mà thay vào đó, cho phép các nhà phát triển dần dần thêm thông tin kiểu vào các biến và biểu thức khi họ thấy phù hợp. Điều này đặc biệt hữu ích cho các cơ sở mã lớn hiện có, nơi người ta có thể kiểm tra tĩnh các phần quan trọng nhất của hệ thống trong khi vẫn để phần còn lại chưa được định kiểu và kiểm tra trong thời gian chạy. Tất cả các giải pháp kiểm tra kiểu cho Ruby mà chúng ta sẽ khám phá trong phần còn lại của bài viết này đều theo cùng một cách tiếp cận.
Tùy chọn
Sau khi tìm hiểu lý do tại sao các nhà phát triển Ruby có thể muốn thêm tính năng kiểm tra kiểu tĩnh vào quy trình phát triển của họ, đã đến lúc khám phá một số tùy chọn phổ biến hiện nay để làm như vậy. Tuy nhiên, điều quan trọng cần lưu ý là ý tưởng thêm kiểm tra kiểu tĩnh vào Ruby không phải là mới. Các nhà nghiên cứu từ Đại học Maryland đã làm việc trên một phần mở rộng Ruby có tên Diamondback Ruby (Druby) vào đầu năm 2009 và Nhóm Ngôn ngữ Lập trình Đại học Tufts đã phát hành một bài báo có tên là Trình kiểm tra Kiểu Ruby vào năm 2013, cuối cùng dẫn đến dự án RDL, cung cấp kiểu kiểm tra và thiết kế các khả năng theo hợp đồng như một thư viện.
Sorbet
Được phát triển bởi Stripe, Sorbet hiện là giải pháp kiểm tra loại được nhắc đến nhiều nhất cho Ruby, đặc biệt là vì các công ty lớn như Shopify, GitLab, Kickstarter và Coinbase đã sớm chấp nhận trong giai đoạn beta kín của nó. Ban đầu nó được công bố trong Ruby Kaigi năm ngoái và được phát hành lần đầu ra công chúng vào ngày 20 tháng 6 năm nay. Sorbet được viết bằng C ++ hiện đại và bất chấp sở thích của Matz (trích dẫn:"Tôi ghét chú thích kiểu"), đã chọn cách tiếp cận dựa trên chú thích kiểu. Một điều đặc biệt thú vị về Sorbet là nó chọn sự kết hợp giữa kiểm tra kiểu tĩnh và động vì bản chất cực kỳ động và khả năng lập trình siêu hình của Ruby là thách thức đối với các hệ thống kiểu tĩnh.
# typed: true
class Test
extend T::Sig
sig {params(x: Integer).returns(String)}
def to_s(x)
x.to_s
end
end
Để bật kiểm tra kiểu, trước tiên chúng ta cần thêm # typed: true
bình luận ma thuật và mở rộng lớp học của chúng tôi với T::Sig
mô-đun. Chú thích loại thực tế được chỉ định bằng sig
phương pháp:
sig {params(x: Integer).returns(String)}
chỉ định rằng phương thức này nhận một đối số duy nhất có tên x
thuộc loại Integer
và trả về một String
. Cố gắng gọi phương thức này với loại đối số sai sẽ dẫn đến lỗi:
Test.new.to_s("42")
# Expected Integer but found String("42") for argument x
Ngoài những cách kiểm tra cơ bản này, Sorbet còn có thêm một vài thủ thuật nữa. Ví dụ:nó có thể cứu chúng ta khỏi NoMethodError
đáng sợ trên nil
:
users = T::Array[User].new
user = users.first
user.username
# Method username does not exist on NilClass component of T.nilable(User)
Đoạn mã trên xác định một mảng trống gồm User
các đối tượng và khi chúng tôi cố gắng truy cập phần tử đầu tiên (phần tử này sẽ trả về nil
) Sorbet cảnh báo chúng tôi một cách chính xác rằng không có phương pháp nào có tên username
có sẵn trên NilClass
. Tuy nhiên, nếu chúng tôi chắc chắn rằng một giá trị nhất định không bao giờ có thể là nil
, chúng ta có thể sử dụng T.must
để cho Sorbet biết điều này:
users = T::Array[User].new
user = T.must(users.first)
user.username
Mặc dù mã ở trên bây giờ sẽ nhập kiểm tra, nhưng nó có thể dẫn đến ngoại lệ thời gian chạy, vì vậy hãy sử dụng tính năng này một cách cẩn thận.
Còn nhiều hơn thế nữa mà Sorbet có thể làm cho chúng ta:phát hiện mã chết, ghim kiểu (về cơ bản gán một biến cho một kiểu nhất định, ví dụ:khi nó đã được gán một chuỗi, nó không bao giờ có thể được gán một số nguyên) hoặc khả năng để xác định giao diện.
Ngoài ra, Sorbet cũng có thể hoạt động với các tệp "Giao diện Ruby" (rbi
) mà nó lưu giữ trong sorbet/
thư mục trong thư mục làm việc hiện tại của bạn. Điều này cho phép chúng tôi tạo các định nghĩa giao diện cho tất cả các gem mà một dự án sử dụng, điều này có thể giúp chúng tôi tìm ra nhiều lỗi loại hơn nữa.
Còn nhiều điều về Sorbet hơn chúng ta có thể trình bày trong một bài viết (ví dụ:các mức độ nghiêm ngặt khác nhau hoặc các plugin lập trình siêu mẫu), nhưng tài liệu của nó đã khá tốt và sẵn sàng cho các hoạt động PR.
Dốc
Giải pháp thay thế được biết đến rộng rãi nhất cho Sorbet là Steep của Soutaro Matsumoto. Nó không sử dụng chú thích và không tự mình thực hiện bất kỳ loại suy luận nào. Thay vào đó, nó hoàn toàn dựa vào .rbi
các tệp trong sig
thư mục.
Hãy bắt đầu từ lớp Ruby đơn giản sau:
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
Giờ đây, chúng tôi có thể tạo ra một user.rbi
ban đầu tệp bằng lệnh sau:
$ steep scaffold user.rb > sig/user.rbi
Điều này dẫn đến tệp sau đây được dùng làm điểm bắt đầu (được minh họa bằng thực tế là tất cả các loại đã được chỉ định là any
, không mang lại sự an toàn):
class User
@first_name: any
@last_name: any
@address: any
def initialize: (any, any, any) -> any
def full_name: () -> String
end
Tuy nhiên, nếu chúng tôi cố gắng nhập kiểm tra tại thời điểm này, chúng tôi sẽ gặp một số lỗi:
$ 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)
Lý do chúng tôi thấy những điều này là vì Steep cần một nhận xét đặc biệt để biết những phương thức nào đã được xác định thông qua attr_reader
s, vì vậy hãy thêm rằng:
# @dynamic first_name, last_name, address
attr_reader :first_name, :last_name, :address
Ngoài ra, chúng ta cần thêm định nghĩa cho các phương thức vào .rbi
đã tạo tập tin. Trong khi chúng ta đang ở đó, hãy cũng thay đổi chữ ký từ any
nào đối với các loại thực tế:
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
Bây giờ, mọi thứ hoạt động như mong đợi và steep check
không trả lại bất kỳ lỗi nào.
Trên hết những gì chúng ta đã thấy cho đến nay, Steep cũng hỗ trợ các số liệu chung (ví dụ:Hash<Symbol, String>
) và các loại kết hợp, đại diện cho một trong hai hoặc sự lựa chọn giữa một số loại. Ví dụ:top_post
của người dùng phương thức có thể trả về bài đăng được xếp hạng cao nhất do người dùng viết hoặc nil
nếu họ chưa đóng góp gì. Điều này được thể hiện thông qua kiểu liên hợp (Post | nil)
và chữ ký tương ứng sẽ giống như sau:
def top_post: () -> (Post | nil)
Mặc dù Steep chắc chắn có ít tính năng hơn Sorbet, nhưng nó vẫn là một công cụ hữu ích và có vẻ phù hợp hơn với những gì Matz đã hình dung về việc kiểm tra kiểu trong Ruby 3.
Hồ sơ loại Ruby
Yusuke Endoh (hay còn được gọi là "mame" trong giới lập trình viên Ruby) từ Cookpad đang làm việc trên cái gọi là trình kiểm tra loại cấp 1 có tên là Ruby Type Profiler. Không giống như các giải pháp khác được trình bày ở đây, nó không cần tệp chữ ký hoặc nhập chú thích mà thay vào đó cố gắng suy ra càng nhiều càng tốt về một chương trình Ruby trong khi phân tích cú pháp. Mặc dù nó ít gặp các vấn đề tiềm ẩn hơn nhiều so với Steep hoặc Sorbet, nhưng nhà phát triển không phải trả thêm phí.
Tóm tắt
Mặc dù không ai có thể đoán trước được tương lai, nhưng có vẻ như việc gõ kiểm tra trong Ruby là một thứ ở đây để tồn tại. Hiện tại, có những nỗ lực đang được tiến hành để chuẩn hóa một "Ngôn ngữ Chữ ký Ruby" để sử dụng trong .rbi
các tệp (có khả năng được tạo bởi Ruby Type Profiler), vì vậy các nhà phát triển có thể sử dụng bất kỳ công cụ nào họ thích. Steep đã cho phép các tác giả thư viện gửi thông tin loại với đá quý của họ và Sorbet có một cơ chế tương tự ở dạng sorbet-type, được lấy cảm hứng từ kho lưu trữ định nghĩa của TypeScript. Nếu bạn quan tâm đến việc giúp định hình tương lai của việc kiểm tra kiểu trong Ruby, bây giờ là thời điểm tuyệt vời để tham gia!