Tôi tự hỏi những lợi thế của Maybe
đơn nguyên so với ngoại lệ là gì? Có vẻ như Maybe
đây chỉ là cách try..catch
cú pháp rõ ràng (và khá tốn dung lượng) .
cập nhật Xin lưu ý rằng tôi cố ý không đề cập đến Haskell.
Tôi tự hỏi những lợi thế của Maybe
đơn nguyên so với ngoại lệ là gì? Có vẻ như Maybe
đây chỉ là cách try..catch
cú pháp rõ ràng (và khá tốn dung lượng) .
cập nhật Xin lưu ý rằng tôi cố ý không đề cập đến Haskell.
Câu trả lời:
Sử dụng Maybe
(hoặc anh em họ của nó Either
hoạt động về cơ bản theo cùng một cách nhưng cho phép bạn trả về một giá trị tùy ý thay cho Nothing
) phục vụ một mục đích hơi khác so với ngoại lệ. Theo thuật ngữ Java, nó giống như có một ngoại lệ được kiểm tra thay vì ngoại lệ thời gian chạy. Nó đại diện cho một cái gì đó được mong đợi mà bạn phải xử lý, thay vì một lỗi bạn không mong đợi.
Vì vậy, một hàm như indexOf
sẽ trả về một Maybe
giá trị vì bạn mong đợi khả năng mục đó không có trong danh sách. Điều này giống như trở về null
từ một chức năng, ngoại trừ theo cách an toàn kiểu buộc bạn phải giải quyết null
vụ việc. Either
hoạt động theo cùng một cách ngoại trừ việc bạn có thể trả về thông tin liên quan đến trường hợp lỗi, vì vậy nó thực sự giống với một ngoại lệ hơn Maybe
.
Vì vậy, những lợi thế của Maybe
/ Either
phương pháp là gì? Đối với một, nó là một công dân hạng nhất của ngôn ngữ. Chúng ta hãy so sánh một hàm sử dụng Either
với một ngoại lệ. Đối với trường hợp ngoại lệ, cách truy đòi thực sự duy nhất của bạn là một try...catch
tuyên bố. Đối với Either
chức năng, bạn có thể sử dụng các tổ hợp hiện có để làm cho điều khiển luồng rõ ràng hơn. Dưới đây là một vài ví dụ:
Trước tiên, giả sử bạn muốn thử một số chức năng có thể bị lỗi liên tiếp cho đến khi bạn nhận được một chức năng không thành công. Nếu bạn không nhận được bất kỳ lỗi nào, bạn muốn trả về một thông báo lỗi đặc biệt. Đây thực sự là một mô hình rất hữu ích nhưng sẽ là một nỗi đau khủng khiếp khi sử dụng try...catch
. Hạnh phúc, vì Either
chỉ là một giá trị bình thường, bạn có thể sử dụng các hàm hiện có để làm cho mã rõ ràng hơn nhiều:
firstThing <|> secondThing <|> throwError (SomeError "error message")
Một ví dụ khác là có một chức năng tùy chọn. Giả sử bạn có một số chức năng để chạy, bao gồm một chức năng cố gắng tối ưu hóa truy vấn. Nếu điều này không thành công, bạn muốn mọi thứ khác chạy bằng mọi cách. Bạn có thể viết mã như:
do a <- getA
b <- getB
optional (optimize query)
execute query a b
Cả hai trường hợp này đều rõ ràng và ngắn hơn so với việc sử dụng try..catch
, và quan trọng hơn là ngữ nghĩa hơn. Sử dụng một chức năng như <|>
hoặc optional
làm cho ý định của bạn nhiều rõ ràng hơn so với sử dụng try...catch
để luôn luôn xử lý ngoại lệ.
Cũng lưu ý rằng bạn không phải vứt mã của mình bằng các dòng như if a == Nothing then Nothing else ...
! Toàn bộ quan điểm đối xử Maybe
và Either
như một đơn nguyên là để tránh điều này. Bạn có thể mã hóa ngữ nghĩa lan truyền vào hàm liên kết để bạn có được kiểm tra null / lỗi miễn phí. Lần duy nhất bạn phải kiểm tra một cách rõ ràng là nếu bạn muốn trả lại một cái gì đó ngoài việc Nothing
được cung cấp Nothing
, và thậm chí sau đó thật dễ dàng: có một loạt các hàm thư viện tiêu chuẩn để làm cho mã đó đẹp hơn.
Cuối cùng, một lợi thế khác là a Maybe
/ Either
type chỉ đơn giản hơn. Không cần phải mở rộng ngôn ngữ với các từ khóa bổ sung hoặc cấu trúc điều khiển - mọi thứ chỉ là một thư viện. Vì chúng chỉ là các giá trị bình thường, nó làm cho hệ thống loại đơn giản hơn - trong Java, bạn phải phân biệt giữa các loại (ví dụ loại trả về) và các hiệu ứng (ví dụ: các throws
câu lệnh) mà bạn sẽ không sử dụng Maybe
. Chúng cũng hoạt động giống như bất kỳ loại do người dùng xác định khác - không cần phải có mã xử lý lỗi đặc biệt được đưa vào ngôn ngữ.
Một chiến thắng khác là Maybe
/ Either
là functor và monad, có nghĩa là họ có thể tận dụng các chức năng dòng điều khiển đơn nguyên hiện có (trong đó có một số công bằng) và nói chung, chơi độc đáo cùng với các đơn nguyên khác.
Điều đó nói rằng, có một số hãy cẩn thận. Đối với một, không Maybe
cũng không Either
thay thế ngoại lệ không được kiểm soát . Bạn sẽ muốn một số cách khác để xử lý những thứ như chia cho 0 đơn giản bởi vì sẽ rất khó để mỗi phân chia trả về một Maybe
giá trị.
Một vấn đề khác là có nhiều loại lỗi trả về (điều này chỉ áp dụng cho Either
). Với các ngoại lệ, bạn có thể ném bất kỳ loại ngoại lệ khác nhau trong cùng một chức năng. với Either
, bạn chỉ có được một loại. Điều này có thể được khắc phục bằng cách gõ phụ hoặc ADT chứa tất cả các loại lỗi khác nhau như các hàm tạo (cách tiếp cận thứ hai này là những gì thường được sử dụng trong Haskell).
Tuy nhiên, trên tất cả, tôi thích cách tiếp cận Maybe
/ Either
vì tôi thấy nó đơn giản và linh hoạt hơn.
OpenFile()
có thể ném FileNotFound
hoặc NoPermission
hoặc TooManyDescriptors
vv Một None không mang thông tin này.if None return None
câu lệnh kiểu.Quan trọng nhất trong tất cả, một ngoại lệ và một đơn vị Có thể có các mục đích khác nhau - một ngoại lệ được sử dụng để biểu thị một vấn đề, trong khi một Có thể không.
"Y tá, nếu có một bệnh nhân ở phòng 5, anh có thể yêu cầu anh ta đợi không?"
(chú ý "nếu" - điều này có nghĩa là bác sĩ đang mong đợi một đơn vị có thể)
None
các giá trị chỉ có thể được truyền bá). Điểm 5 của bạn chỉ là loại câu hỏi đúng, câu hỏi nào là: tình huống nào rõ ràng đặc biệt? Hóa ra là không nhiều .
bind
theo cách mà việc kiểm tra None
không phát sinh chi phí cú pháp. Một ví dụ rất đơn giản, C # chỉ quá tải các Nullable
toán tử một cách thích hợp. Không kiểm tra None
cần thiết, ngay cả khi sử dụng loại. Tất nhiên việc kiểm tra vẫn được thực hiện (loại an toàn), nhưng đằng sau hậu trường và không làm lộn xộn mã của bạn. Điều tương tự cũng áp dụng theo một cách nào đó đối với sự phản đối của bạn đối với sự phản đối của tôi đối với (5) nhưng tôi đồng ý rằng điều đó có thể không phải lúc nào cũng áp dụng.
Maybe
như một đơn nguyên là làm cho việc truyền bá None
ngầm. Điều này có nghĩa rằng nếu bạn muốn quay trở lại None
được None
, bạn không cần phải viết bất kỳ mã đặc biệt nào cả. Thời gian duy nhất bạn cần để phù hợp là nếu bạn muốn làm một cái gì đó đặc biệt trên None
. Bạn không bao giờ cần if None then None
loại báo cáo.
null
kiểm tra chính xác như thế (ví dụ if Nothing then Nothing
) miễn phí vì Maybe
là một đơn nguyên. Nó được mã hóa theo định nghĩa của bind ( >>=
) cho Maybe
.
Either
) hoạt động giống như vậy Maybe
. Chuyển đổi giữa hai thực sự khá đơn giản vì Maybe
thực sự chỉ là một trường hợp đặc biệt Either
. (Trong Haskell, bạn có thể nghĩ Maybe
như Either ()
.)
"Có thể" không phải là sự thay thế cho các trường hợp ngoại lệ. Các ngoại lệ được sử dụng trong các trường hợp ngoại lệ (ví dụ: mở kết nối db và máy chủ db không có ở đó mặc dù vậy). "Có thể" là để mô hình hóa một tình huống khi bạn có thể có hoặc không có giá trị hợp lệ; nói rằng bạn đang nhận được một giá trị từ một từ điển cho một khóa: nó có thể ở đó hoặc có thể không - không có gì "đặc biệt" về bất kỳ kết quả nào trong số này.
Tôi thứ hai câu trả lời của Tikhon, nhưng tôi nghĩ có một điểm thực tế rất quan trọng mà mọi người đều thiếu:
Either
chế không được kết hợp với chủ đề nào cả.Vì vậy, một điều mà chúng ta đang thấy trong cuộc sống thực là nhiều giải pháp lập trình không đồng bộ đang áp dụng một biến thể của Either
kiểu xử lý lỗi. Hãy xem xét các lời hứa Javascript , như chi tiết trong bất kỳ liên kết nào sau đây:
Khái niệm về lời hứa cho phép bạn viết mã không đồng bộ như thế này (lấy từ liên kết cuối cùng):
var greetingPromise = sayHello();
greetingPromise
.then(addExclamation)
.then(function (greeting) {
console.log(greeting); // 'hello world!!!!’
}, function(error) {
console.error('uh oh: ', error); // 'uh oh: something bad happened’
});
Về cơ bản, một lời hứa là một đối tượng:
Về cơ bản, vì hỗ trợ ngoại lệ gốc của ngôn ngữ không hoạt động khi tính toán của bạn xảy ra trên nhiều luồng, nên việc triển khai hứa hẹn phải cung cấp cơ chế xử lý lỗi và các biến này trở thành các đơn vị tương tự như Maybe
/ Either
loại của Haskell .
Hệ thống loại Haskell sẽ yêu cầu người dùng thừa nhận khả năng của a Nothing
, trong khi các ngôn ngữ lập trình thường không yêu cầu bắt ngoại lệ. Điều đó có nghĩa là chúng ta sẽ biết, tại thời điểm biên dịch, người dùng đã kiểm tra lỗi.
throws NPE
chữ ký và catch(...) {throw ...}
vào mỗi thân phương thức. Nhưng tôi tin rằng có một thị trường để kiểm tra theo nghĩa tương tự như Có thể: vô hiệu là tùy chọn và được theo dõi trong hệ thống loại.
Đơn nguyên có thể về cơ bản giống như việc kiểm tra "null có nghĩa là lỗi" của ngôn ngữ chính thống nhất (ngoại trừ yêu cầu kiểm tra null) và phần lớn có những ưu điểm và nhược điểm giống nhau.
Maybe
số bằng cách viết a + b
mà không cần kiểm tra None
và kết quả là một lần nữa là một giá trị tùy chọn.
Maybe
loại , nhưng sử dụng Maybe
như một đơn nguyên sẽ thêm đường cú pháp cho phép diễn đạt logic null-ish thanh lịch hơn rất nhiều.
Xử lý ngoại lệ có thể là một nỗi đau thực sự cho bao thanh toán và thử nghiệm. Tôi biết python cung cấp cú pháp "với" tốt cho phép bạn bẫy các ngoại lệ mà không cần khối "thử ... bắt" cứng nhắc. Nhưng trong Java, ví dụ, hãy thử các khối bắt lớn, luồn lách, dài dòng hoặc cực kỳ dài dòng và khó chia tay. Trên hết, Java thêm tất cả nhiễu xung quanh các ngoại lệ được kiểm tra so với không được kiểm tra.
Thay vào đó, nếu, đơn nguyên của bạn bắt được các ngoại lệ và coi chúng là tài sản của không gian đơn nguyên (thay vì một số dị thường xử lý), thì bạn có thể tự do trộn và khớp các chức năng mà bạn liên kết vào không gian đó bất kể chúng ném hay bắt.
Nếu, tốt hơn nữa, đơn nguyên của bạn ngăn chặn các điều kiện trong đó các trường hợp ngoại lệ có thể xảy ra (như đẩy một kiểm tra null vào Có thể), thì thậm chí còn tốt hơn. nếu ... thì rất nhiều, dễ dàng hơn nhiều để thử nghiệm và thử nghiệm hơn là thử ... bắt.
Từ những gì tôi thấy Go đang thực hiện một cách tiếp cận tương tự bằng cách chỉ định rằng mỗi hàm trả về (câu trả lời, lỗi). Điều đó giống như "nâng" chức năng vào một không gian đơn nguyên trong đó loại câu trả lời cốt lõi được trang trí với một dấu hiệu lỗi, và ném ngoại lệ và bắt ngoại lệ một cách hiệu quả.