Có lẽ đơn nguyên và ngoại lệ


Câu trả lời:


57

Sử dụng Maybe(hoặc anh em họ của nó Eitherhoạ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ư indexOfsẽ trả về một Maybegiá 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ề nulltừ 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 nullvụ việc. Eitherhoạ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/ Eitherphươ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 Eithervớ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...catchtuyên bố. Đối với Eitherchứ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ì Eitherchỉ 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 optionallà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ử MaybeEithernhư 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/ Eithertype 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 throwscâ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/ Eitherlà 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 Maybecũng không Eitherthay 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 Maybegiá 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/ Eithervì tôi thấy nó đơn giản và linh hoạt hơn.


Hầu hết đều đồng ý, nhưng tôi không nghĩ ví dụ "tùy chọn" có ý nghĩa - có vẻ như bạn cũng có thể làm điều đó với các ngoại lệ: void Tùy chọn (Hành động hành động) {thử {hành động (); } Catch (Exception) {}}
Erroratz

11
  1. Một ngoại lệ có thể mang thêm thông tin về nguồn gốc của một vấn đề. OpenFile()có thể ném FileNotFoundhoặc NoPermissionhoặc TooManyDescriptorsvv Một None không mang thông tin này.
  2. Các ngoại lệ có thể được sử dụng trong các bối cảnh thiếu giá trị trả về (ví dụ với các hàm tạo trong các ngôn ngữ có chúng).
  3. Một ngoại lệ cho phép bạn rất dễ dàng gửi thông tin lên ngăn xếp, mà không có nhiều if None return Nonecâu lệnh kiểu.
  4. Xử lý ngoại lệ hầu như luôn mang lại tác động hiệu suất cao hơn là chỉ trả về một giá trị.
  5. 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?"

    • Có lẽ đơn nguyên: "Bác sĩ ơi, không có bệnh nhân ở phòng 5."
    • Ngoại lệ: "Bác sĩ, không có phòng 5!"

    (chú ý "nếu" - điều này có nghĩa là bác sĩ đang mong đợi một đơn vị có thể)


7
Tôi không đồng ý với điểm 2 và 3. Đặc biệt, 2 không thực sự gây ra vấn đề trong các ngôn ngữ sử dụng đơn nguyên vì các ngôn ngữ đó không có quy ước không tạo ra câu hỏi hóc búa khi phải xử lý thất bại trong quá trình xây dựng. Liên quan đến 3, các ngôn ngữ với các đơn nguyên có cú pháp phù hợp để xử lý các đơn vị một cách minh bạch mà không yêu cầu kiểm tra rõ ràng (ví dụ: Nonecá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 .
Konrad Rudolph

3
Về (2), bạn có thể viết bindtheo cách mà việc kiểm tra Nonekhô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 Nullabletoán tử một cách thích hợp. Không kiểm tra Nonecầ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.
Konrad Rudolph

4
@Oak: Về (3), toàn bộ quan điểm đối xử Maybenhư một đơn nguyên là làm cho việc truyền bá Nonengầ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 Noneloại báo cáo.
Tikhon Jelvis

5
@Oak: đó là sự thật, nhưng đó không thực sự là quan điểm của tôi. Điều tôi đang nói là bạn được nullkiểm tra chính xác như thế (ví dụ if Nothing then Nothing) miễn phíMaybelà một đơn nguyên. Nó được mã hóa theo định nghĩa của bind ( >>=) cho Maybe.
Tikhon Jelvis

2
Ngoài ra, liên quan đến (1): bạn có thể dễ dàng viết một đơn nguyên có thể mang thông tin lỗi (ví dụ Either) hoạt động giống như vậy Maybe. Chuyển đổi giữa hai thực sự khá đơn giản vì Maybethực sự chỉ là một trường hợp đặc biệt Either. (Trong Haskell, bạn có thể nghĩ Maybenhư Either ().)
Tikhon Jelvis

6

"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.


2
Trên thực tế, tôi có khá nhiều mã liên quan đến từ điển trong đó một khóa bị thiếu có nghĩa là công cụ đã bị sai một cách khủng khiếp tại một số điểm, rất có thể là lỗi trong mã của tôi hoặc trong mã đã chuyển đổi đầu vào thành bất cứ thứ gì được sử dụng tại thời điểm đó.

3
@delnan: Tôi không nói rằng một giá trị bị thiếu không bao giờ có thể là dấu hiệu của một trạng thái đặc biệt - nó không nhất thiết phải như vậy. Có thể thực sự mô hình hóa một tình huống trong đó một biến có thể có hoặc không có giá trị hợp lệ - nghĩ rằng các loại không có giá trị trong C #.
Nemanja Trifunovic

6

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:

  1. Các cơ chế xử lý ngoại lệ, ít nhất là trong các ngôn ngữ chính, được kết hợp mật thiết với các luồng riêng lẻ.
  2. Eitherchế 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 Eitherkiể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:

  1. Thể hiện kết quả của một tính toán không đồng bộ, có thể hoặc chưa thể kết thúc;
  2. Cho phép bạn xâu chuỗi các hoạt động tiếp theo để thực hiện theo kết quả của nó, sẽ được kích hoạt khi kết quả đó khả dụng và kết quả lần lượt có sẵn như lời hứa;
  3. Cho phép bạn kết nối một trình xử lý thất bại sẽ được gọi nếu tính toán của lời hứa thất bại. Nếu không có trình xử lý, thì lỗi sẽ được truyền đến các trình xử lý sau trong chuỗi.

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/ Eitherloại của Haskell .


Nó không có gì để làm với chủ đề. JavaScript trong trình duyệt luôn chạy trong một luồng không phải trên nhiều luồng. Nhưng bạn vẫn không thể sử dụng ngoại lệ vì bạn không biết khi nào chức năng của bạn được gọi trong tương lai. Không đồng bộ không tự động có nghĩa là sự tham gia của các chủ đề. Đó cũng là lý do tại sao bạn không thể làm việc với các ngoại lệ. Bạn chỉ có thể tìm nạp một ngoại lệ nếu bạn gọi một hàm và ngay lập tức nó được thực thi. Nhưng toàn bộ mục đích của Không đồng bộ là nó chạy trong tương lai, thường là khi một cái gì đó khác hoàn thành, và không ngay lập tức. Đó là lý do tại sao bạn không thể sử dụng ngoại lệ ở đó.
David Raab

1

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.


1
Có các ngoại lệ được kiểm tra trong Java. Và mọi người thường đối xử tệ với họ.
Vladimir

1
@ DairT'arg ButJava yêu cầu kiểm tra lỗi IO (làm ví dụ), nhưng không phải cho NPE. Trong Haskell, cách khác là: Kiểm tra tương đương null (Có thể) được kiểm tra nhưng lỗi IO chỉ đơn giản là ném ngoại lệ (không được kiểm tra). Tôi không chắc có bao nhiêu sự khác biệt tạo ra, nhưng tôi không nghe thấy Haskeller phàn nàn về Có lẽ và tôi nghĩ rằng rất nhiều người muốn kiểm tra NPE.

1
@delnan Mọi người muốn kiểm tra NPE? Họ phải điên lên. Và tôi có nghĩa là. Việc kiểm tra NPE chỉ có ý nghĩa nếu bạn có cách tránh nó ngay từ đầu (bằng cách có các tham chiếu không thể rỗng, mà Java không có).
Konrad Rudolph

5
@KonradRudolph Có, mọi người có thể không muốn kiểm tra theo nghĩa là họ sẽ phải thêm một throws NPEchữ 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.

1
@del Nam ơi, rồi em đồng ý.
Konrad Rudolph

0

Đơ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.


8
Chà, nó không có nhược điểm tương tự vì nó có thể được kiểm tra kiểu tĩnh khi được sử dụng đúng cách. Không có tương đương với một ngoại lệ con trỏ null khi sử dụng các đơn vị có thể (một lần nữa, giả sử chúng được sử dụng đúng cách).
Konrad Rudolph

Thật. Làm thế nào để bạn nghĩ rằng cần được làm rõ?
Telastyn

@Konrad Đó là vì để sử dụng giá trị (có thể) trong Có thể, bạn phải kiểm tra Không (mặc dù tất nhiên có khả năng tự động hóa rất nhiều kiểm tra đó). Các ngôn ngữ khác giữ loại hướng dẫn sử dụng đó (chủ yếu là vì lý do triết học mà tôi tin).
Donal Fellows

2
@Donal Có nhưng lưu ý rằng hầu hết các ngôn ngữ triển khai các đơn nguyên đều cung cấp đường cú pháp phù hợp để kiểm tra này thường có thể bị ẩn hoàn toàn. Ví dụ, bạn chỉ có thể thêm hai Maybesố bằng cách viết a + b mà không cần kiểm tra Nonevà kết quả là một lần nữa là một giá trị tùy chọn.
Konrad Rudolph

2
Việc so sánh với các loại nullable đúng với Maybe loại , nhưng sử dụng Maybenhư 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.
tdammers

0

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ả.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.