Tại sao sử dụng Ngoại lệ (đã kiểm tra)?


17

Cách đây không lâu, tôi đã bắt đầu sử dụng Scala thay vì Java. Một phần của quá trình "chuyển đổi" giữa các ngôn ngữ đối với tôi là học cách sử dụng Eithers thay vì (đã kiểm tra) Exception. Tôi đã viết mã theo cách này được một thời gian, nhưng gần đây tôi bắt đầu tự hỏi liệu đó có thực sự là một cách tốt hơn để đi không.

Một lợi thế lớn Eithercó hơn Exceptionlà hiệu suất tốt hơn; một Exceptionnhu cầu để xây dựng một dấu vết ngăn xếp và đang bị ném. Theo như tôi hiểu, mặc dù, ném phần Exceptionkhông phải là đòi hỏi, nhưng xây dựng dấu vết ngăn xếp là.

Nhưng sau đó, người ta luôn có thể xây dựng / kế thừa Exceptions scala.util.control.NoStackTrace, và thậm chí nhiều hơn thế, tôi thấy rất nhiều trường hợp trong đó bên trái của một Eitherthực tế là một Exception(cho phép tăng hiệu suất).

Một lợi thế khác Eitherlà an toàn trình biên dịch; trình biên dịch Scala sẽ không phàn nàn về các trình xử lý Exceptionkhông (không giống như trình biên dịch của Java). Nhưng nếu tôi không nhầm, quyết định này được đưa ra với cùng lý do đang được thảo luận trong chủ đề này, vì vậy ...

Về mặt cú pháp, tôi cảm thấy như Exceptionkiểu là rõ ràng hơn. Kiểm tra các khối mã sau (cả hai đều đạt được cùng chức năng):

Either Phong cách:

def compute(): Either[String, Int] = {

    val aEither: Either[String, String] = if (someCondition) Right("good") else Left("bad")

    val bEithers: Iterable[Either[String, Int]] = someSeq.map {
        item => if (someCondition(item)) Right(item.toInt) else Left("bad")
    }

    for {
        a <- aEither.right
        bs <- reduce(bEithers).right
        ignore <- validate(bs).right
    } yield compute(a, bs)
}

def reduce[A,B](eithers: Iterable[Either[A,B]]): Either[A, Iterable[B]] = ??? // utility code

def validate(bs: Iterable[Int]): Either[String, Unit] = if (bs.sum > 22) Left("bad") else Right()

def compute(a: String, bs: Iterable[Int]): Int = ???

Exception Phong cách:

@throws(classOf[ComputationException])
def compute(): Int = {

    val a = if (someCondition) "good" else throw new ComputationException("bad")
    val bs = someSeq.map {
        item => if (someCondition(item)) item.toInt else throw new ComputationException("bad")
    }

    if (bs.sum > 22) throw new ComputationException("bad")

    compute(a, bs)
}

def compute(a: String, bs: Iterable[Int]): Int = ???

Cái sau có vẻ sạch hơn đối với tôi và mã xử lý lỗi (khớp mẫu trên Eitherhoặc try-catch) khá rõ ràng trong cả hai trường hợp.

Vì vậy, câu hỏi của tôi là - tại sao sử dụng Eitherhơn (kiểm tra) Exception?

Cập nhật

Sau khi đọc câu trả lời, tôi nhận ra rằng tôi có thể đã thất bại trong việc trình bày cốt lõi của tình huống khó xử của mình. Mối quan tâm của tôi là không có việc thiếu các try-catch; một có thể "bắt" một Exceptionvới Try, hoặc sử dụng catchđể bọc các ngoại lệ với Left.

Vấn đề chính của tôi với Either/ Tryxuất hiện khi tôi viết mã có thể thất bại tại nhiều điểm trên đường đi; trong các kịch bản này, khi gặp phải một thất bại, tôi phải tuyên truyền sự thất bại đó trong toàn bộ mã của mình, do đó làm cho mã trở nên cồng kềnh hơn (như trong các ví dụ đã nói ở trên).

Thực sự có một cách khác để phá mã mà không cần Exceptions bằng cách sử dụng return(thực tế là một "điều cấm kỵ" khác trong Scala). Mã sẽ vẫn rõ ràng hơn so với Eithercách tiếp cận, và trong khi sạch hơn một chút so với Exceptionkiểu, sẽ không sợ Exceptions không bị bắt .

def compute(): Either[String, Int] = {

  val a = if (someCondition) "good" else return Left("bad")

  val bs: Iterable[Int] = someSeq.map {
    item => if (someCondition(item)) item.toInt else return Left("bad")
  }

  if (bs.sum > 22) return Left("bad")

  val c = computeC(bs).rightOrReturn(return _)

  Right(computeAll(a, bs, c))
}

def computeC(bs: Iterable[Int]): Either[String, Int] = ???

def computeAll(a: String, bs: Iterable[Int], c: Int): Int = ???

implicit class ConvertEither[L, R](either: Either[L, R]) {

  def rightOrReturn(f: (Left[L, R]) => R): R = either match {
    case Right(r) => r
    case Left(l) => f(Left(l))
  }
}

Về cơ bản, các return Leftthay thế throw new Exceptionvà phương thức ngầm định rightOrReturnlà một phần bổ sung cho việc truyền ngoại lệ tự động lên ngăn xếp.



@RobertHarvey Có vẻ như hầu hết các bài viết thảo luận Try. Phần về Eithervs Exceptionchỉ đơn thuần nói rằng Eithers nên được sử dụng khi trường hợp khác của phương thức là "không đặc biệt". Đầu tiên, đây là một định nghĩa rất, rất mơ hồ. Thứ hai, nó có thực sự đáng bị phạt cú pháp không? Ý tôi là, tôi thực sự không phiền khi sử dụng Eithers nếu nó không dùng cho cú pháp mà họ trình bày.
Eyal Roth

Eithertrông giống như một đơn nguyên đối với tôi. Sử dụng nó khi bạn cần các lợi ích thành phần chức năng mà các đơn vị cung cấp. Hoặc, có thể không .
Robert Harvey

2
@RobertHarvey: Về mặt kỹ thuật, Eitherbản thân nó không phải là một đơn nguyên. Hình chiếu sang bên trái hoặc bên phải là một đơn nguyên, nhưng Eitherbản thân nó thì không. Bạn có thể làm chotrở thành một đơn nguyên, bằng cách "thiên vị" nó sang bên trái hoặc bên phải, mặc dù. Tuy nhiên, sau đó bạn truyền đạt một ngữ nghĩa nhất định trên cả hai mặt của một Either. Scala Eitherban đầu không thiên vị, nhưng gần đây đã bị thiên vị, vì vậy mà ngày nay, nó thực sự là một đơn nguyên, nhưng "sự đơn điệu" không phải là một tài sản vốn có Eithermà là kết quả của nó bị thiên vị.
Jörg W Mittag

@ JörgWMittag: Tốt, tốt. Điều đó có nghĩa là bạn có thể sử dụng nó cho tất cả các ưu điểm đơn nguyên của nó và không bị làm phiền đặc biệt bởi cách "làm sạch" mã kết quả (mặc dù tôi nghi ngờ rằng mã tiếp tục kiểu chức năng thực sự sẽ sạch hơn phiên bản Exception).
Robert Harvey

Câu trả lời:


13

Nếu bạn chỉ sử dụng một khối Eitherchính xác như một try-catchkhối bắt buộc , tất nhiên nó sẽ trông giống như các cú pháp khác nhau để thực hiện chính xác cùng một điều. Eitherslà một giá trị . Họ không có những hạn chế giống nhau. Bạn có thể:

  • Dừng thói quen giữ khối cố gắng của bạn nhỏ và tiếp giáp. Phần "bắt" Eitherskhông cần phải nằm ngay bên cạnh phần "thử". Nó thậm chí có thể được chia sẻ trong một chức năng phổ biến. Điều này làm cho nó dễ dàng hơn nhiều để phân tách mối quan tâm đúng cách.
  • Lưu trữ Eitherstrong cấu trúc dữ liệu. Bạn có thể lặp qua chúng và hợp nhất các thông báo lỗi, tìm thông báo lỗi đầu tiên không thất bại, đánh giá chúng một cách lười biếng hoặc tương tự.
  • Mở rộng và tùy chỉnh chúng. Đừng thích một số bản tóm tắt mà bạn buộc phải viết Eithers? Bạn có thể viết các hàm trợ giúp để làm việc với chúng dễ dàng hơn cho các trường hợp sử dụng cụ thể của bạn. Không giống như thử bắt, bạn không bị kẹt vĩnh viễn chỉ với giao diện mặc định mà các nhà thiết kế ngôn ngữ đã đưa ra.

Lưu ý đối với hầu hết các trường hợp sử dụng, Trygiao diện đơn giản hơn, có hầu hết các lợi ích trên và thường đáp ứng nhu cầu của bạn. An Optionthậm chí còn đơn giản hơn nếu tất cả những gì bạn quan tâm là thành công hay thất bại và không cần bất kỳ thông tin trạng thái nào như thông báo lỗi về trường hợp thất bại.

Theo kinh nghiệm của tôi, Eitherskhông thực sự được ưa thích hơn Trystrừ khi Leftđó là một cái gì đó không phải Exceptionlà một thông báo lỗi xác thực và bạn muốn tổng hợp các Leftgiá trị. Ví dụ: bạn đang xử lý một số đầu vào và bạn muốn thu thập tất cả các thông báo lỗi, không chỉ lỗi ở lần đầu tiên. Những tình huống đó không xuất hiện thường xuyên trong thực tế, ít nhất là đối với tôi.

Nhìn xa hơn mô hình thử bắt và bạn sẽ thấy Eithersdễ chịu hơn khi làm việc.

Cập nhật: câu hỏi này vẫn đang được chú ý và tôi đã không thấy bản cập nhật của OP làm rõ câu hỏi cú pháp, vì vậy tôi nghĩ tôi sẽ thêm vào.

Như một ví dụ về việc thoát ra khỏi suy nghĩ ngoại lệ, đây là cách tôi thường viết mã ví dụ của bạn:

def compute(): Either[String, Int] = {
  Right(someSeq)
    .filterOrElse(someACondition,            "someACondition failed")
    .filterOrElse(_ forall someSeqCondition, "someSeqCondition failed")
    .map(_ map {_.toInt})
    .filterOrElse(_.sum <= 22, "Sum is greater than 22")
    .map(compute("good",_))
}

Ở đây bạn có thể thấy rằng ngữ nghĩa của filterOrElseviệc nắm bắt hoàn hảo những gì chúng ta chủ yếu làm trong chức năng này: kiểm tra các điều kiện và liên kết các thông báo lỗi với các lỗi đã cho. Vì đây là trường hợp sử dụng rất phổ biến, filterOrElsenằm trong thư viện chuẩn, nhưng nếu không, bạn có thể tạo nó.

Đây là điểm mà một người nào đó đưa ra một ví dụ ngược lại không thể giải quyết chính xác giống như của tôi, nhưng điểm tôi đang cố gắng thực hiện là không chỉ có một cách sử dụng Eithers, không giống như ngoại lệ. Có nhiều cách để đơn giản hóa mã cho hầu hết mọi tình huống xử lý lỗi, nếu bạn khám phá tất cả các chức năng có sẵn để sử dụng Eithersvà mở để thử nghiệm.


1. Có thể phân tách trycatchlà một lý lẽ công bằng, nhưng điều này không thể dễ dàng đạt được với Try? 2. Việc lưu trữ Eithers trong cấu trúc dữ liệu có thể được thực hiện cùng với throwscơ chế; không phải nó chỉ đơn giản là một lớp bổ sung trên đầu xử lý thất bại sao? 3. Bạn chắc chắn có thể mở rộng Exceptions. Chắc chắn, bạn không thể thay đổi try-catchcấu trúc, nhưng việc xử lý các lỗi không phổ biến đến mức tôi chắc chắn muốn ngôn ngữ mang lại cho tôi một bản tóm tắt cho điều đó (mà tôi không bị buộc phải sử dụng). Điều đó giống như nói rằng sự hiểu biết là xấu bởi vì nó là một cái nồi hơi cho các hoạt động đơn nguyên.
Eyal Roth

Tôi chỉ nhận ra rằng bạn nói rằng Eithers có thể được mở rộng, trong đó rõ ràng là chúng không thể (là một sealed traitchỉ có hai triển khai, cả hai final). bạn có thể giải thích về điều đó không? Tôi cho rằng bạn không có nghĩa là nghĩa đen của việc mở rộng.
Eyal Roth

1
Tôi có nghĩa là mở rộng như trong từ tiếng Anh, không phải từ khóa lập trình. Thêm chức năng bằng cách tạo các chức năng để xử lý các mẫu phổ biến.
Karl Bielefeldt

8

Hai thực sự rất khác nhau. EitherNgữ nghĩa của nó là tổng quát hơn nhiều so với chỉ đại diện cho các tính toán có khả năng thất bại.

Eithercho phép bạn trả về một trong hai loại khác nhau. Đó là nó.

Bây giờ, một trong hai loại đó có thể hoặc không thể đại diện cho một lỗi và loại kia có thể hoặc không thể đại diện cho thành công, nhưng đó chỉ là một trường hợp sử dụng có thể có của nhiều người. Eitherlà nhiều, nói chung nhiều hơn thế. Bạn cũng có thể có một phương thức đọc đầu vào từ người dùng được phép nhập tên hoặc trả lại ngày Either[String, Date].

Hoặc, nghĩ về chữ lambda trong C♯: họ đánh giá theo một Funchoặc một Expression, tức là họ đánh giá một hàm ẩn danh hoặc họ đánh giá AST. Trong C♯, điều này xảy ra "kỳ diệu", nhưng nếu bạn muốn đưa ra một loại thích hợp cho nó, nó sẽ như vậy Either<Func<T1, T2, …, R>, Expression<T1, T2, …, R>>. Tuy nhiên, cả hai tùy chọn này đều không có lỗi và cả hai tùy chọn này đều "bình thường" hoặc "ngoại lệ". Chúng chỉ là hai loại khác nhau có thể được trả về từ cùng một chức năng (hoặc trong trường hợp này, được gán cho cùng một nghĩa đen). [Lưu ý: thực tế, Funcphải được chia thành hai trường hợp khác, vì C♯ không có Unitloại và do đó, thứ gì đó không trả lại bất cứ thứ gì cũng không thể được trình bày giống như cách trả lại thứ gì đó,Either<Either<Func<T1, T2, …, R>, Action<T1, T2, …>>, Expression<T1, T2, …, R>>

Bây giờ, Either có thể được sử dụng để đại diện cho một loại thành công và một loại thất bại. Và nếu bạn đồng ý về một thứ tự nhất quán của hai loại, bạn thậm chí có thể thiên vị Either để thích một trong hai loại, do đó có thể truyền bá thất bại thông qua chuỗi đơn âm. Nhưng trong trường hợp đó, bạn đang truyền đạt một ngữ nghĩa nhất định Eithermà nó không nhất thiết phải sở hữu.

Một loại Eithercó một trong hai loại được cố định là loại lỗi và bị lệch về một phía để truyền lỗi, thường được gọi là Errorđơn nguyên và sẽ là lựa chọn tốt hơn để biểu diễn một tính toán có khả năng thất bại. Trong Scala, loại này được gọi là Try.

Nó có ý nghĩa hơn nhiều để so sánh việc sử dụng các ngoại lệ với Tryhơn Either.

Vì vậy, đây là lý do số 1 tại sao Eithercó thể được sử dụng trên các ngoại lệ được kiểm tra: Eitherchung chung hơn nhiều so với ngoại lệ. Nó có thể được sử dụng trong trường hợp ngoại lệ thậm chí không áp dụng.

Tuy nhiên, rất có thể bạn đã hỏi về trường hợp bị hạn chế khi bạn chỉ sử dụng Either(hoặc nhiều khả năng hơn Try) để thay thế cho các trường hợp ngoại lệ. Trong trường hợp đó, vẫn có một số lợi thế, hoặc các cách tiếp cận khác nhau có thể.

Ưu điểm chính là bạn có thể trì hoãn xử lý lỗi. Bạn chỉ cần lưu trữ giá trị trả về, thậm chí không cần xem đó là thành công hay lỗi. (Xử lý nó sau.) Hoặc vượt qua nó ở một nơi khác. (Để người khác lo lắng về nó.) Thu thập chúng trong một danh sách. (Xử lý tất cả chúng cùng một lúc.) Bạn có thể sử dụng chuỗi đơn âm để truyền chúng dọc theo đường ống xử lý.

Các ngữ nghĩa của các trường hợp ngoại lệ được gắn chặt vào ngôn ngữ. Trong Scala, ví dụ, thư giãn ngăn xếp, ví dụ. Nếu bạn để chúng nổi bong bóng lên một trình xử lý ngoại lệ, thì trình xử lý đó không thể quay trở lại nơi xảy ra và sửa nó. OTOH, nếu bạn bắt nó thấp hơn trong ngăn xếp trước khi tháo gỡ nó và phá hủy bối cảnh cần thiết để khắc phục nó, thì bạn có thể ở trong một thành phần quá thấp để đưa ra quyết định sáng suốt về cách tiến hành.

Đây là khác nhau trong Smalltalk, ví dụ, trường hợp ngoại lệ không thư giãn stack và Resumable, và bạn có thể bắt các cao ngoại lệ lên trong ngăn xếp (có thể là lên cao như debugger), sửa chữa lên lỗi waaaaayyyyy xuống trong ngăn xếp và tiếp tục thực hiện từ đó. Hoặc điều kiện CommonLisp trong đó nuôi, bắt và xử lý là ba điều khác nhau thay vì hai (nuôi và bắt-xử lý) như với các ngoại lệ.

Sử dụng loại trả về lỗi thực tế thay vì ngoại lệ cho phép bạn thực hiện những việc tương tự và hơn thế nữa mà không phải sửa đổi ngôn ngữ.

Và sau đó có một con voi rõ ràng trong phòng: ngoại lệ là một tác dụng phụ. Tác dụng phụ là xấu xa. Lưu ý: Tôi không nói rằng điều này nhất thiết phải đúng. Nhưng đó là một quan điểm mà một số người có, và trong trường hợp đó, lợi thế của một loại lỗi so với các ngoại lệ là rõ ràng. Vì vậy, nếu bạn muốn lập trình chức năng, bạn có thể sử dụng các loại lỗi vì lý do duy nhất là chúng là phương pháp tiếp cận chức năng. (Lưu ý rằng Haskell có ngoại lệ. Cũng lưu ý rằng không ai thích sử dụng chúng.)


Yêu câu trả lời, mặc dù tôi vẫn không hiểu tại sao tôi nên từ bỏ Exception. Eithercó vẻ như là một công cụ tuyệt vời, nhưng vâng, tôi đặc biệt hỏi về việc xử lý các thất bại và tôi muốn sử dụng một công cụ cụ thể hơn nhiều cho nhiệm vụ của mình hơn là một công cụ toàn năng. Trì hoãn xử lý thất bại là một lý lẽ công bằng, nhưng người ta có thể chỉ cần sử dụng Trymột phương pháp ném Exceptionnếu anh ta muốn không xử lý nó vào lúc này. Không ngăn xếp theo dõi ngăn xếp ảnh hưởng Either/ Trylà tốt? Bạn có thể lặp lại những sai lầm tương tự khi tuyên truyền chúng không chính xác; biết khi nào để xử lý một thất bại là không tầm thường trong cả hai trường hợp.
Eyal Roth

Và tôi thực sự không thể tranh luận với lý luận "hoàn toàn chức năng", phải không?
Eyal Roth

0

Điều tuyệt vời về các trường hợp ngoại lệ là mã nguồn gốc đã phân loại hoàn cảnh đặc biệt cho bạn. Sự khác biệt giữa một IllegalStateExceptionvà một BadParameterExceptionlà dễ dàng hơn nhiều để phân biệt trong các ngôn ngữ gõ mạnh hơn. Nếu bạn cố gắng phân tích "tốt" và "xấu", bạn sẽ không tận dụng lợi thế của công cụ cực kỳ mạnh mẽ này, cụ thể là trình biên dịch Scala. Các tiện ích mở rộng của riêng bạn Throwablecó thể bao gồm nhiều thông tin bạn cần, ngoài chuỗi tin nhắn văn bản.

Đây là tiện ích thực sự Trytrong môi trường Scala, với Throwablevai trò là lớp bọc của các vật phẩm bên trái để làm cho cuộc sống dễ dàng hơn.


2
Tôi không hiểu quan điểm của bạn.
Eyal Roth

Eithercó vấn đề về phong cách vì nó không phải là một đơn nguyên thực sự (?); tính tùy tiện của leftgiá trị tránh xa giá trị gia tăng cao hơn khi bạn gửi thông tin chính xác về các điều kiện đặc biệt trong một cấu trúc phù hợp. Vì vậy, so sánh việc sử dụng Eithertrong một trường hợp trả về các chuỗi ở phía bên trái, so với try/catchtrong trường hợp khác sử dụng cách gõ đúng Throwables là không phù hợp. Do giá trị của Throwablesviệc cung cấp thông tin và cách Tryxử lý ngắn gọn Throwables, Trynên đặt cược tốt hơn so với ... Eitherhoặctry/catch
BobDalgleish

Tôi thực sự không quan tâm đến try-catch. Như đã nói trong câu hỏi, cái sau có vẻ rõ ràng hơn đối với tôi và mã xử lý lỗi (khớp mẫu trên Either hoặc try-Catch) khá rõ ràng trong cả hai trường hợp.
Eyal Roth
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.