Не так давно я начал использовать Scala вместо Java. Частью процесса «преобразования» между языками для меня было обучение использованию Either
s вместо (проверенных) Exception
s. Некоторое время я так кодировал, но недавно начал задаваться вопросом, действительно ли это лучший путь.
Одним из основных преимуществ Either
имеет более Exception
лучше производительность; Exception
необходимо построить стек-след и бросают. Насколько я понимаю, бросать Exception
не является сложной задачей, но построение трассировки стека.
Но потом, всегда можно построить / унаследует Exception
s с scala.util.control.NoStackTrace
, и даже более того, я вижу много случаев , когда левая сторона Either
фактически являетсяException
(исключая повышение производительности).
Еще одно преимущество Either
- безопасность компилятора; компилятор Scala не будет жаловаться на необработанные Exception
s (в отличие от компилятора 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
with Try
, либо использовать его catch
для обёртывания исключения Left
.
Моя главная проблема с Either
/ Try
возникает, когда я пишу код, который может потерпеть неудачу во многих местах; в этих сценариях, при обнаружении сбоя, я должен распространять этот сбой по всему моему коду, что делает код более громоздким (как показано в вышеупомянутых примерах).
На самом деле есть еще один способ взломать код без Exception
s с помощью return
(что на самом деле является еще одним «табу» в Scala). Код был бы все же более понятным, чем Either
подход, и, будучи немного менее чистым, чем Exception
стиль, не было бы страха перед не пойманными Exception
s.
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
являются дополнением к автоматическому распространению исключений в стеке.
источник
Try
. Часть оEither
vsException
просто утверждает, чтоEither
s следует использовать, когда другой случай метода является «неисключительным». Во-первых, это очень, очень расплывчатое определение imho. Во-вторых, действительно ли стоит штраф за синтаксис? Я имею в виду, я бы не стал возражать против использованияEither
s, если бы не накладные расходы на синтаксис, которые они представляют.Either
выглядит как монада для меня. Используйте его, когда вам нужны преимущества функциональной композиции, которые дают монады. Или, может быть, нет .Either
сам по себе не является монадой. Проекция либо на левой стороне или на правой стороне является монадой, ноEither
сама по себе не является. Вы можете сделать это монадой, «смещая» ее либо к левой стороне, либо к правой стороне. Однако затем вы привносите определенную семантику по обе стороны отEither
.Either
Изначально Scala был беспристрастным, но был предвзятым сравнительно недавно, так что в настоящее время он фактически является монадой, но «монадность» не является присущим свойством,Either
а скорее является результатом его предвзятости.Ответы:
Если вы используете только один
Either
и тот же императивныйtry-catch
блок, конечно, будет выглядеть другой синтаксис, чтобы делать точно то же самое.Eithers
являются ценностью . У них нет одинаковых ограничений. Вы можете:Eithers
не обязательно должна быть рядом с частью «try». Он может даже использоваться в общей функции. Это значительно облегчает разделение проблем должным образом.Eithers
в структурах данных. Вы можете просмотреть их и объединить сообщения об ошибках, найти первое, которое не удалось, лениво оценить их или подобное.Eithers
? Вы можете написать вспомогательные функции, чтобы облегчить их работу для ваших конкретных случаев использования. В отличие от try-catch, вы не зависаете только с интерфейсом по умолчанию, который разработчики языка придумали.Обратите внимание, что для большинства случаев использования
Try
интерфейс имеет более простой интерфейс, обладает большинством вышеперечисленных преимуществ и обычно также соответствует вашим потребностям. ЕщеOption
проще, если все, что вас волнует - это успех или неудача, и вам не нужна информация о состоянии, например, сообщения об ошибках в случае сбоя.По моему опыту, на
Eithers
самом деле не предпочтительнее,Trys
если только этоLeft
не что-то иноеException
, возможно, сообщение об ошибке проверки, и вы хотите объединитьLeft
значения. Например, вы обрабатываете некоторые входные данные и хотите собрать все сообщения об ошибках, а не только об ошибках в первом. Такие ситуации не часто возникают на практике, по крайней мере, для меня.Посмотрите на модель try-catch, и вам станет
Eithers
намного приятнее работать.Обновление: этот вопрос все еще привлекает внимание, и я не видел, чтобы обновление ОП разъясняло вопрос о синтаксисе, поэтому я решил добавить больше.
В качестве примера выхода из менталитета исключений, я обычно пишу ваш пример кода:
Здесь вы можете увидеть, что семантика
filterOrElse
прекрасно отражает то, что мы в основном делаем в этой функции: проверка условий и связывание сообщений об ошибках с заданными сбоями. Так как это очень распространенный вариант использования,filterOrElse
находится в стандартной библиотеке, но если это не так, вы можете создать его.Это тот момент, когда кто-то придумывает контрпример, который не может быть решен точно так же, как мой, но я пытаюсь подчеркнуть, что
Eithers
, в отличие от исключений, не существует единственного способа использования . Существуют способы упростить код для большинства ситуаций обработки ошибок, если вы изучите все функции, доступные для использования,Eithers
и открыты для экспериментов.источник
try
иcatch
является справедливым аргументом, но разве это не может быть легко достигнутоTry
? 2. ХранениеEither
s в структурах данных может осуществляться вместе сthrows
механизмом; Разве это не просто дополнительный слой поверх обработки ошибок? 3. Вы можете определенно продлитьException
с. Конечно, вы не можете изменитьtry-catch
структуру, но обработка сбоев настолько распространена, что я определенно хочу, чтобы язык дал мне образец для этого (который я не вынужден использовать). Это все равно, что сказать, что понимание - это плохо, потому что это шаблон для монадических операций.Either
s можно расширять там, где они явно не могут (будучиsealed trait
только с двумя реализациями, обеfinal
). Можете ли вы уточнить это? Я предполагаю, что вы не имели в виду буквальное значение расширения.Два на самом деле очень разные.
Either
Семантика гораздо более общая, чем просто представление потенциально неудачных вычислений.Either
позволяет вернуть любой из двух разных типов. Вот и все.Теперь, один из этих двух типов может представлять или не представлять ошибку, а другой может или не может представлять успех, но это только один из многих возможных вариантов использования.
Either
гораздо, гораздо более общий, чем это. Вы также можете иметь метод, который читает ввод от пользователя, которому разрешено либо ввести имя, либо дату, возвращающуюEither[String, Date]
.Или подумайте о лямбда-литералах в C♯: они либо оцениваются как an,
Func
либо anExpression
, то есть они оценивают анонимную функцию или 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
а? Вы можете повторять одни и те же ошибки при неправильном их распространении; Знание того, когда нужно решить проблему, не является тривиальным в обоих случаях.Приятная особенность исключений состоит в том, что исходный код уже классифицировал исключительные обстоятельства для вас. Различие между «
IllegalStateException
а» и «а»BadParameterException
намного легче различить в более типизированных языках. Если вы попытаетесь разобрать «хорошо» и «плохо», вы не воспользуетесь чрезвычайно мощным инструментом, а именно компилятором Scala. Ваши собственные расширенияThrowable
могут включать столько информации, сколько вам нужно, в дополнение к текстовой строке сообщения.Это настоящая полезность
Try
в среде Scala, которая служитThrowable
оберткой для оставленных предметов, чтобы облегчить жизнь.источник
Either
имеет проблемы со стилем, потому что это не настоящая монада (?); произвольностьleft
значения отдаляется от более высокой добавленной стоимости, когда вы должным образом заключаете информацию об исключительных условиях в надлежащую структуру. Поэтому сравнение использованияEither
возвращаемых строк в одном случае с левой стороны по сравнениюtry/catch
с другим использованием неправильно введенных символовThrowables
не является правильным. Из-за ценностиThrowables
предоставления информации и лаконичного способаTry
обработкиThrowables
,Try
это лучшая ставка, чем либо ...,Either
либоtry/catch
try-catch
. Как сказано в вопросе, последний выглядит для меня намного чище, и код, обрабатывающий ошибку (либо сопоставление с шаблоном в Either, либо try-catch), достаточно ясен в обоих случаях.