Навіщо використовувати виключення (перевірено)?


17

Не так давно я почав використовувати Scala замість Java. Частиною процесу "перетворення" між мовами для мене було навчитися використовувати Eithers замість (перевірених) Exceptions. Я кодував цей спосіб деякий час, але останнім часом почав цікавитись, чи справді це кращий шлях.

Одним з основних переваг Eitherмає більш Exceptionкраще продуктивність; Exceptionнеобхідно побудувати стек-слід і кидають. Наскільки я розумію, однак, кидання Exceptionне найвибагливішої частини, але побудова сліду стека є.

Але тоді, завжди можна побудувати / успадкувати Exceptions scala.util.control.NoStackTrace, і тим більше, я бачу безліч випадків, коли ліва сторона an Eitherнасправді є Exception(відмови від підвищення продуктивності).

Ще одна перевага Either- безпека компілятора; компілятор Scala не поскаржиться на не оброблені Exceptions (на відміну від компілятора Java). Але якщо я не помиляюся, це рішення мотивоване тими ж міркуваннями, про які йде мова в цій темі, тож ...

З точки зору синтаксису, я відчуваю, що Exceptionстиль - ясніше. Вивчіть наступні блоки коду (обидва досягнення однакової функціональності):

Either стиль:

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 стиль:

@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 = ???

Останній виглядає для мене набагато чистішим, і код, який поводиться з помилкою (або узгодженням зразком, Eitherабо try-catch), досить чіткий в обох випадках.

Отже, моє запитання - навіщо використовувати Either(перевірено) Exception?

Оновлення

Прочитавши відповіді, я зрозумів, що я, можливо, не зміг представити суть своєї дилеми. Моя стурбованість не полягає у відсутності try-catch; можна або "зловити" за Exceptionдопомогою Try, або використовувати його, catchщоб обернути виняток Left.

Моя основна проблема з Either/ Tryвиникає, коли я пишу код, який може вийти з ладу в багатьох моментах; у цих сценаріях, коли я стикаюся з помилкою, я повинен розповсюджувати цей збій у всьому своєму коді, тим самим роблячи шлях коду більш громіздким (як показано у вищезгаданих прикладах).

Насправді існує інший спосіб зламати код без Exceptions за допомогою return(що насправді є ще одним «табу» у Scala). Код буде все-таки чіткішим за Eitherпідхід, і, хоча бути трохи менш чистим, ніж Exceptionстиль, не було б страху перед не зловилими Exceptions.

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))
  }
}

В основному return Leftзаміна throw new Exception, і неявний метод для будь-якого rightOrReturn, є доповненням до автоматичного поширення винятків до стека.



@RobertHarvey Здається, більшість статей обговорюється Try. Частина про Eithervs Exceptionпросто зазначає, що Eithers слід використовувати, коли інший випадок методу є "невиключним". По-перше, це дуже і дуже розпливчасте визначення імхо. По-друге, чи дійсно варто синтаксичне покарання? Я маю на увазі, я дійсно не був би проти використання Eithers, якби не синтаксис, який вони представляють.
Eyal Roth

Eitherмені схожа на монаду. Використовуйте його, коли потрібні переваги функціонального складу, які надають монади. А може, ні .
Роберт Харві

2
@RobertHarvey: Технічно кажучи, Eitherсама по собі не є монадою. Проекція або на лівій стороні або на правій стороні є монадой, але Eitherсама по собі не є. Ви можете зробити його монадою, "відхиливши" її вліво чи вправо. Однак тоді ви надаєте певну семантику з обох сторін Either. EitherСпочатку Скала був неупередженим, але був упередженим досить недавно, так що в даний час це насправді монада, але "монадність" не є властивою властивістю, Eitherа скоріше є результатом його упередженості.
Йорг W Міттаг

@ JörgWMittag: Ну добре. Це означає, що ви можете використовувати його для всієї його монадійної користі, і не особливо вас турбує те, наскільки "чистить" отриманий код (хоча я підозрюю, що код продовження функціонального стилю насправді був би чистішим за версію винятку).
Роберт Харві

Відповіді:


13

Якщо ви використовуєте лише такий Either, як імперативний try-catchблок, звичайно, це буде виглядати як різний синтаксис, щоб зробити саме те саме. Eithersє цінністю . Вони не мають однакових обмежень. Ти можеш:

  • Перестаньте звичати зберігати пробні блоки невеликими та суміжними. Частина "ловлі" Eithersне повинна бути поруч із частиною "спробувати". Це навіть може бути спільним для загальної функції. Це значно полегшує належне відокремлення проблем.
  • Зберігати Eithersв структурах даних. Ви можете проглядати їх і консолідувати повідомлення про помилки, знаходити перше, що не вийшло з ладу, ліниво оцінювати їх або подібне.
  • Розширюйте та налаштовуйте їх. Вам не подобається якась плита, з якою ви змушені писати Eithers? Ви можете записати допоміжні функції, щоб полегшити роботу з вашими конкретними випадками використання. На відміну від пробного лову, ви не назавжди застрягли лише з інтерфейсом за замовчуванням, який створили дизайнери мови.

Зауважте, що для більшості випадків використання A Tryмає більш простий інтерфейс, має більшість перерахованих вище переваг і зазвичай так само відповідає вашим потребам. Це Optionще простіше, якщо все, що вас цікавить, - це успіх чи невдача і не потрібна інформація про стан, як-от повідомлення про помилки щодо випадку відмови.

На мій досвід, Eithersнасправді не віддають перевагу над, Trysякщо тільки Leftщось інше, ніж повідомлення Exception, можливо, повідомлення про помилку перевірки, і ви хочете зібрати Leftзначення. Наприклад, ви обробляєте деякий вхід і хочете зібрати всі повідомлення про помилки, а не лише помилки на першому. Такі ситуації на практиці не трапляються дуже часто, принаймні для мене.

Подивіться за межі спробної моделі, і вам буде Eithersнабагато приємніше працювати.

Оновлення: це питання все ще привертає увагу, і я не бачив оновлення ОП, яке б роз'яснювало питання синтаксису, тому я подумав, що додам ще.

Як приклад виходу з режиму мислення на винятки, я зазвичай пишу ваш приклад код:

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",_))
}

Тут ви бачите, що семантика filterOrElseпрекрасно фіксує те, що ми в основному робимо в цій функції: перевірка умов та асоціювання повідомлень про помилки із заданими відмовами. Оскільки це дуже поширений випадок використання, filterOrElseвін знаходиться в стандартній бібліотеці, але якщо цього не було, ви можете створити його.

Це той момент, коли хтось придумує зустрічний приклад, який неможливо вирішити точно так само, як мій, але пункт, який я намагаюся зробити, - це не один спосіб використання Eithers, на відміну від винятків. Існують способи спростити код для більшості будь-яких ситуацій з помилками, якщо вивчити всі функції, доступні для використання, Eithersі відкриті для експериментів.


1. Вміти відокремити tryта catchє справедливим аргументом, але чи не вдалося цього досягти легко Try? 2. Зберігання Eitherданих у структурах даних може здійснюватися поряд із throwsмеханізмом; це не просто зайвий шар, що перевищує збої в роботі? 3. Ви точно можете продовжити Exceptions. Звичайно, ви не можете змінити try-catchструктуру, але збої в роботі настільки поширені, що я, безумовно, хочу, щоб мова дала мені котельну табличку для цього (що я не змушений використовувати). Це як би сказати, що для розуміння це погано, оскільки це котельня для монадичних операцій.
Ейал Рот

Я щойно зрозумів, що ви сказали, що Eithers можна розширити, де явно вони не можуть (будучи sealed traitлише двома реалізаціями, обидва final). Чи можете ви детальніше розглянути це? Я припускаю, що ви не мали на увазі буквального значення розширення.
Ейал Рот

1
Я мав на увазі розширений, як в англійському слові, а не ключове слово програмування. Додавання функціональності шляхом створення функцій для обробки загальних шаблонів.
Карл Білефельдт

8

Два насправді дуже різні. EitherСемантика Росії набагато загальніша, ніж просто представити потенційно невдалі обчислення.

Eitherдозволяє повернути один із двох різних типів. Це воно.

Тепер один з цих двох типів може або не може представляти помилку, а другий може або не може представляти успіх, але це лише один можливий випадок використання багатьох. Eitherє набагато, набагато загальнішим за це. Ви також можете застосувати метод, який зчитує дані від користувача, якому дозволено вводити ім'я або повертати дату Either[String, Date].

Або думати про лямбда - литералов в C♯: вони або обчислюватися в Funcабо Expression, тобто вони або обчислюватися анонімної функції або вони оцінюють на АСТ. У C♯ це відбувається "магічно", але якби ви хотіли надати йому належного типу, це було б Either<Func<T1, T2, …, R>, Expression<T1, T2, …, R>>. Однак жоден з двох варіантів не є помилкою, і жоден із двох варіантів не є "нормальним" або "винятковим". Вони є лише двома різними типами, які можуть бути повернуті з однієї і тієї ж функції (або в цьому випадку віднесені до одного буквального). [Примітка: насправді Funcтреба розділити ще два випадки, оскільки C♯ не має Unitтипу, і тому щось, що нічого не повертає, не може бути представлене так само, як щось, що щось повертає,Either<Either<Func<T1, T2, …, R>, Action<T1, T2, …>>, Expression<T1, T2, …, R>>

Тепер Either можна використовувати для представлення типу успіху та типу відмови. І якщо ви погоджуєтесь на послідовне впорядкування двох типів, ви можете навіть упереджувати Either перевагу одного з двох типів, таким чином дозволяючи поширювати невдачі за допомогою монадичного ланцюга. Але в цьому випадку ви надаєте певну семантику Eitherтому, що воно не обов'язково володіє самостійно.

Те, Eitherщо має один з двох його типів, зафіксованим як тип помилки, і є упередженою стороною для поширення помилок, зазвичай називається Errorмонадою, і було б кращим вибором для представлення потенційно невдалого обчислення. У Скалі такий тип називають Try.

Це робить багато сенсу більше , щоб порівняти використання винятків з Tryчим Either.

Отже, це причина №1, чому Eitherможна використовувати над перевіреними винятками: Eitherнабагато загальніша за винятки. Його можна використовувати в тих випадках, коли винятки навіть не застосовуються.

Однак ви, швидше за все, запитували про обмежений випадок, коли ви просто використовуєте Either(або скоріше Try) як заміну винятків. У цьому випадку все ж є певні переваги, а точніше різні підходи.

Основна перевага полягає в тому, що ви можете відкласти поводження з помилкою. Ви просто зберігаєте повернене значення, навіть не дивлячись, чи це успіх чи помилка. (Обробіть його пізніше.) Або перенесіть його десь інше. (Нехай хтось інший хвилюється про це.) Зберіть їх у списку. (Обробіть їх усі одразу.) Ви можете використовувати монадичні ланцюги, щоб розповсюдити їх уздовж трубопроводу для обробки.

Семантика винятків прокладена в мову. Наприклад, у Scala винятки розмотують стек, наприклад. Якщо ви дозволите їм перекинутися на обробку винятків, тоді обробник не зможе повернутися вниз, де це сталося, і виправити це. ОТОХ, якщо ви зафіксуєте його нижче в стеку перед тим, як розмотати його та знищити необхідний контекст для його виправлення, ви, можливо, будете в компоненті, який є занадто низьким, щоб прийняти зважене рішення про те, як діяти далі.

Це відрізняється в Smalltalk, наприклад, коли виняток, не розкрутка стеки і поновлювані, і ви можете зловити максимум виключення в стеці (можливо , як високо, як відладчик), виправити помилку waaaaayyyyy вниз в стек і відновити виконання звідти. Або умови CommonLisp, коли підняття, лов та поводження - це три різні речі замість двох (підняття та обробка), як за винятками.

Використання фактичного типу повернення помилок замість винятку дозволяє робити подібні речі та інше, не змінюючи мову.

І тоді в кімнаті є очевидний слон: винятки є побічним ефектом. Побічні ефекти - це зло. Примітка. Я не кажу, що це обов'язково так. Але це точка зору, яку мають деякі люди, і в цьому випадку перевага типу помилки перед винятками очевидна. Отже, якщо ви хочете займатися функціональним програмуванням, ви можете використовувати типи помилок з єдиної причини, що вони є функціональним підходом. (Зауважте, що у Haskell є винятки. Зауважте також, що ніхто не любить ними користуватися.)


Люблю відповідь, хоча я досі не бачу, чому я повинен відмовитись Exception. Eitherздається, що це чудовий інструмент, але так, я спеціально запитую про помилки, і я вважаю за краще використовувати набагато більш конкретний інструмент для своєї задачі, ніж всесильний. Віднесення поводження з помилками - справедливий аргумент, але можна просто скористатися Tryметодом викидання, Exceptionякщо він бажає не впоратися з цим на даний момент. Чи не впливає Either/ Tryтакож накручування слідів на сліди ? Ви можете повторити ті самі помилки, коли їх неправильно поширювати; знати, коли впоратися з невдачею, в обох випадках не тривіально.
Ейал Рот

І я не можу насправді сперечатися з "чисто функціональними" міркуваннями, чи не можу?
Ейал Рот

0

Приємна річ у винятках полягає в тому, що вихідний код вже класифікував виняткову обставину для вас. Різницю між a IllegalStateExceptionі a BadParameterExceptionнабагато простіше розрізнити в сильніших набраних мовах. Якщо ви намагаєтеся розібрати "хороший" і "поганий", ви не скористаєтесь надзвичайно потужним інструментом, який є у вашому розпорядженні, а саме - компілятором Scala. Ваші власні розширення до Throwableможуть включати стільки інформації, скільки вам потрібно, крім рядка текстового повідомлення.

Це справжня корисність Tryу середовищі Scala, яка Throwableвиступає в якості обгортки лівих предметів, щоб полегшити життя.


2
Я не розумію вашої точки зору.
Ейал Рот

Eitherмає проблеми зі стилем, оскільки це не справжня монада (?); довільність leftвартості відхиляється від вищої доданої вартості, коли ви належним чином додаєте інформацію про виняткові умови у відповідній структурі. Тому порівнювати використання Eitherв одному випадку рядків, що повертаються в лівій частині, і try/catchв іншому випадку з правильним набором тексту Throwables не є правильним. З - за вартості Throwablesдля надання інформації, і короткій формі , що Tryручки Throwables, Tryє кращим вибором , ніж або ... Eitherабоtry/catch
BobDalgleish

Мене насправді не хвилює try-catch. Як сказано в питанні, останнє виглядає для мене набагато чіткіше, і код, що поводиться з помилкою (або узгодження шаблону на Either, або try-catch) є досить зрозумілим в обох випадках.
Ейал Рот
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.