Зачем использовать исключение Either over (проверено)?

17

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

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

Но потом, всегда можно построить / унаследует Exceptions с scala.util.control.NoStackTrace, и даже более того, я вижу много случаев , когда левая сторона 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; Можно либо «поймать» оператор Exceptionwith 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являются дополнением к автоматическому распространению исключений в стеке.

Эяль Рот
источник
Читать это: mauricio.github.io/2014/02/17/…
Роберт Харви
@RobertHarvey Похоже, что большая часть статьи обсуждает Try. Часть о Eithervs Exceptionпросто утверждает, что Eithers следует использовать, когда другой случай метода является «неисключительным». Во-первых, это очень, очень расплывчатое определение imho. Во-вторых, действительно ли стоит штраф за синтаксис? Я имею в виду, я бы не стал возражать против использования Eithers, если бы не накладные расходы на синтаксис, которые они представляют.
Эяль Рот
Eitherвыглядит как монада для меня. Используйте его, когда вам нужны преимущества функциональной композиции, которые дают монады. Или, может быть, нет .
Роберт Харви,
2
@RobertHarvey: Технически говоря, Eitherсам по себе не является монадой. Проекция либо на левой стороне или на правой стороне является монадой, но Eitherсама по себе не является. Вы можете сделать это монадой, «смещая» ее либо к левой стороне, либо к правой стороне. Однако затем вы привносите определенную семантику по обе стороны от Either. EitherИзначально Scala был беспристрастным, но был предвзятым сравнительно недавно, так что в настоящее время он фактически является монадой, но «монадность» не является присущим свойством, Eitherа скорее является результатом его предвзятости.
Йорг Миттаг,
@ JörgWMittag: Ну хорошо. Это означает, что вы можете использовать его для всей его монадичности, и не особенно беспокоиться о том, насколько «чист» получающийся код (хотя я подозреваю, что код продолжения функционального стиля на самом деле будет чище, чем версия Exception).
Роберт Харви,

Ответы:

13

Если вы используете только один Eitherи тот же императивный try-catchблок, конечно, будет выглядеть другой синтаксис, чтобы делать точно то же самое. Eithersявляются ценностью . У них нет одинаковых ограничений. Вы можете:

  • Прекратите свою привычку держать свои пробные блоки маленькими и смежными. Часть «catch» Eithersне обязательно должна быть рядом с частью «try». Он может даже использоваться в общей функции. Это значительно облегчает разделение проблем должным образом.
  • Хранить Eithersв структурах данных. Вы можете просмотреть их и объединить сообщения об ошибках, найти первое, которое не удалось, лениво оценить их или подобное.
  • Расширяйте и настраивайте их. Не нравится какой-то шаблон, с которым ты вынужден писать Eithers? Вы можете написать вспомогательные функции, чтобы облегчить их работу для ваших конкретных случаев использования. В отличие от try-catch, вы не зависаете только с интерфейсом по умолчанию, который разработчики языка придумали.

Обратите внимание, что для большинства случаев использования Tryинтерфейс имеет более простой интерфейс, обладает большинством вышеперечисленных преимуществ и обычно также соответствует вашим потребностям. Еще Optionпроще, если все, что вас волнует - это успех или неудача, и вам не нужна информация о состоянии, например, сообщения об ошибках в случае сбоя.

По моему опыту, на Eithersсамом деле не предпочтительнее, Trysесли только это Leftне что-то иное Exception, возможно, сообщение об ошибке проверки, и вы хотите объединить Leftзначения. Например, вы обрабатываете некоторые входные данные и хотите собрать все сообщения об ошибках, а не только об ошибках в первом. Такие ситуации не часто возникают на практике, по крайней мере, для меня.

Посмотрите на модель try-catch, и вам станет 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. Хранение Eithers в структурах данных может осуществляться вместе с throwsмеханизмом; Разве это не просто дополнительный слой поверх обработки ошибок? 3. Вы можете определенно продлить Exceptionс. Конечно, вы не можете изменить try-catchструктуру, но обработка сбоев настолько распространена, что я определенно хочу, чтобы язык дал мне образец для этого (который я не вынужден использовать). Это все равно, что сказать, что понимание - это плохо, потому что это шаблон для монадических операций.
Эяль Рот
Я только что понял, что вы сказали, что Eithers можно расширять там, где они явно не могут (будучи sealed traitтолько с двумя реализациями, обе final). Можете ли вы уточнить это? Я предполагаю, что вы не имели в виду буквальное значение расширения.
Эяль Рот
1
Я имел в виду расширенный, как в английском слове, а не ключевое слово программирования. Добавление функциональности путем создания функций для обработки общих шаблонов.
Карл Билефельдт
8

Два на самом деле очень разные. EitherСемантика гораздо более общая, чем просто представление потенциально неудачных вычислений.

Eitherпозволяет вернуть любой из двух разных типов. Вот и все.

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

Или подумайте о лямбда-литералах в C♯: они либо оцениваются как an, Funcлибо an Expression, то есть они оценивают анонимную функцию или AST. В 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монадой и будет лучшим выбором для представления потенциально неудачных вычислений. В Scala этот тип называется Try.

Гораздо больше смысла сравнивать использование исключений с, Tryчем с Either.

Итак, это причина № 1, почему Eitherможет быть использовано над проверенными исключениями:Either гораздо более общая, чем исключения. Он может использоваться в тех случаях, когда исключения даже не применяются.

Тем не менее, вы, скорее всего, спрашивали об ограниченном случае, когда вы просто используете Either(или более вероятно Try) в качестве замены исключений. В этом случае все еще есть некоторые преимущества или, скорее, другие подходы.

Основным преимуществом является то, что вы можете отложить обработку ошибки. Вы просто сохраняете возвращаемое значение, даже не смотря на то, является ли это успехом или ошибкой. (Обращайтесь позже.) Или передайте это куда-нибудь еще. (Пусть кто-то еще беспокоится об этом.) Соберите их в список. (Обработайте их все сразу.) Вы можете использовать монадическую цепочку для распространения их по конвейеру обработки.

Семантика исключений встроена в язык. Например, в Scala исключения разматывают стек. Если вы позволите им всплыть в обработчике исключений, то обработчик не сможет вернуться туда, где это произошло, и исправить это. OTOH, если вы поймаете его ниже в стеке, прежде чем раскрутить его и уничтожить необходимый контекст, чтобы исправить это, то вы можете оказаться в компоненте, который находится на слишком низком уровне, чтобы принять обоснованное решение о том, как действовать дальше.

Это отличается в Smalltalk, например, где исключения не разматывают стек и являются возобновляемыми, и вы можете перехватить исключение высоко в стеке (возможно, так же высоко, как отладчик), исправить ошибку waaaaayyyyy вниз в стек и возобновить выполнение оттуда. Или условия CommonLisp, где поднятие, улавливание и обработка - это три разные вещи вместо двух (поднятие и обработка-отлов), как с исключениями.

Использование фактического типа возврата ошибки вместо исключения позволяет вам делать подобные вещи и многое другое, без необходимости изменять язык.

И тут в комнате очевидный слон: исключения - это побочный эффект. Побочные эффекты являются злом. Примечание: я не говорю, что это обязательно так. Но это точка зрения некоторых людей, и в этом случае преимущество типа ошибки перед исключениями очевидно. Итак, если вы хотите заниматься функциональным программированием, вы можете использовать типы ошибок только по той причине, что они являются функциональным подходом. (Обратите внимание, что у Haskell есть исключения. Обратите внимание, что никто не любит их использовать.)

Йорг Миттаг
источник
Мне нравится ответ, хотя я до сих пор не понимаю, почему я должен отказываться Exception. Eitherкажется хорошим инструментом, но да, я специально спрашиваю об обработке сбоев, и я предпочел бы использовать гораздо более конкретный инструмент для своей задачи, чем всесильный. Откладывание обработки сбоев является справедливым аргументом, но можно просто использовать Tryметод, который выдает, Exceptionесли он не хочет обрабатывать это в данный момент. Не стек трассировки размотки аффекта Either/ Tryа? Вы можете повторять одни и те же ошибки при неправильном их распространении; Знание того, когда нужно решить проблему, не является тривиальным в обоих случаях.
Эяль Рот
И я не могу спорить с «чисто функциональными» рассуждениями, не так ли?
Эяль Рот
0

Приятная особенность исключений состоит в том, что исходный код уже классифицировал исключительные обстоятельства для вас. Различие между « IllegalStateExceptionа» и «а» BadParameterExceptionнамного легче различить в более типизированных языках. Если вы попытаетесь разобрать «хорошо» и «плохо», вы не воспользуетесь чрезвычайно мощным инструментом, а именно компилятором Scala. Ваши собственные расширения Throwableмогут включать столько информации, сколько вам нужно, в дополнение к текстовой строке сообщения.

Это настоящая полезность Tryв среде Scala, которая служит Throwableоберткой для оставленных предметов, чтобы облегчить жизнь.

BobDalgleish
источник
2
Я не понимаю вашу точку зрения.
Эяль Рот
Eitherимеет проблемы со стилем, потому что это не настоящая монада (?); произвольность leftзначения отдаляется от более высокой добавленной стоимости, когда вы должным образом заключаете информацию об исключительных условиях в надлежащую структуру. Поэтому сравнение использования Eitherвозвращаемых строк в одном случае с левой стороны по сравнению try/catchс другим использованием неправильно введенных символов Throwables не является правильным. Из-за ценности Throwablesпредоставления информации и лаконичного способа Tryобработки Throwables, Tryэто лучшая ставка, чем либо ..., Eitherлибоtry/catch
BobDalgleish
Я на самом деле не беспокоюсь о try-catch. Как сказано в вопросе, последний выглядит для меня намного чище, и код, обрабатывающий ошибку (либо сопоставление с шаблоном в Either, либо try-catch), достаточно ясен в обоих случаях.
Эяль Рот