Почему следует выбирать вариант для обработки ошибок над исключениями в Scala?

Итак, я изучаю функциональный Scala, и в книге говорится, что исключение разбивает ссылочную прозрачность, и поэтому Option следует использовать вместо этого:

def pattern(s: String): Option[Pattern] = {
  try {
    Some(Pattern.compile(s))
  } catch {
    case e: PatternSyntaxException => None
  }
}

Это выглядит довольно плохо; Я имею в виду, что это похоже на:

catch(Exception e){
    return null;
}

Сохранить для того, чтобы мы могли отличить "null для ошибки" от "null как подлинное значение". Кажется, он должен хотя бы вернуть что-то, что содержит информацию об ошибке, например:

catch {
    case e: Exception => Fail(e)
}

Что мне не хватает?

Ответ 1

В этом конкретном разделе Option используется в основном в качестве примера, потому что используемая операция (вычисление mean) является частичной функцией, она не дает значения для всех возможных значений (сбор может быть пустым, таким образом, нет способа вычислить среднее значение), а Option может быть справедливым случаем здесь. Если вы не можете вычислить mean, потому что коллекция пуста, просто верните None.

Но есть много других способов решить эту проблему, вы можете использовать Either[L,R], при этом результат Left является результатом ошибки, а Right - хорошим результатом, вы все равно можете выбросить исключение и обернуть его внутри объекта Try (что сейчас кажется более распространенным из-за его использования в вычислениях Promise и Future), вы можете использовать ScalaZ Validation, если ошибка была на самом деле проблема проверки.

Основная идея, которую вы должны сделать из этой части, состоит в том, что ошибка должна быть частью возвращаемого типа функции, а не некоторой магической операции (исключение), которая не может быть разумно объявлена ​​типами.

И как бесстыдный плагин, я сделал блог о Либо и попробуйте здесь.

Ответ 2

Было бы легче ответить на этот вопрос, если бы вы не спрашивали: "Почему Option лучше, чем исключения?" и "почему Option лучше нуля?" и "почему Option лучше, чем Try?" все в то же время.

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

Ответ на второй вопрос (почему не null?) - это что-то вроде "Вам когда-нибудь приходилось иметь дело с NullPointerException в Java?".

Для третьего вопроса, в общем, вы правы - лучше использовать тип типа Either[Throwable, A] или Try[A] для представления вычислений, которые могут быть неудачными, поскольку они позволяют вам передавать более подробную информацию об ошибке. В некоторых случаях, однако, когда функция может только терпеть неудачу одним очевидным способом, имеет смысл использовать Option. Например, если я выполняю поиск на карте, мне, вероятно, не нужно или нужно что-то вроде Either[NoSuchElementException, A], где ошибка настолько абстрактна, что я, вероятно, в конечном итоге завершу ее в нечто большее, в любом случае. Так что get на карте просто возвращает Option[A].

Ответ 3

Вы должны использовать util.Try:

scala> import java.util.regex.Pattern
import java.util.regex.Pattern

scala> def pattern(s: String): util.Try[Pattern] = util.Try(Pattern.compile(s))
pattern: (s: String)scala.util.Try[java.util.regex.Pattern]


scala> pattern("<?++")
res0: scala.util.Try[java.util.regex.Pattern] =
Failure(java.util.regex.PatternSyntaxException: Dangling meta character '+' near index 3
<?++
   ^)

scala> pattern("[.*]")
res1: scala.util.Try[java.util.regex.Pattern] = Success([.*])

Ответ 4

Наивный пример

def pattern(s: String): Pattern = {
  Pattern.compile(s)
}

имеет побочный эффект, он может влиять на программу, которая использует ее другими способами, чем ее результат (это может вызвать исключение). Это обескураживает функциональное программирование, потому что это увеличивает сложность кода.
Код

def pattern(s: String): Option[Pattern] = {
  try {
    Some(Pattern.compile(s))
  } catch {
    case e: PatternSyntaxException => None
  }
}

инкапсулирует побочный эффект, производящий часть программы. Информация о том, почему шаблон не удалась, потеряна, но иногда это имеет значение только в случае ее отказа. Если это важно, почему метод не удалось, вы можете использовать Try (http://www.scala-lang.org/files/archive/nightly/docs/library/index.html#scala.util.Try):

def pattern(s: String): Try[Pattern] = {
   Try(Pattern.compile(s))
}

Ответ 5

Я думаю, что два других ответа дают вам хорошие предложения о том, как действовать. Я бы все же утверждал, что бросание исключения хорошо представлено в системе типа Scala, используя нижний тип Nothing. Так что он хорошо напечатан, и я бы точно не назвал его "волшебной операцией".

Однако... если ваш метод может довольно часто приводить к недопустимому значению, то есть если ваша сторона вызова вполне разумно хочет обрабатывать такое недопустимое значение сразу, то используя Option, Either или Try является хорошим подходом. В сценарии, где ваш сайт вызова действительно не знает, что делать с таким недопустимым значением, особенно если это исключительное условие, а не обычный случай, тогда вы должны использовать исключения IMO.

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

Если какие-либо функции, которые делают что-либо с целыми числами, объявят свой возвращаемый тип a Try из-за возможностей деления на нуль или переполнения, это может полностью загромождать ваш код. Еще одна очень важная причина использования исключений - недопустимые диапазоны аргументов или требования. Если вы ожидаете, что аргумент будет целым числом между 0 и x, вы можете выбросить IllegalArgumentException, если он не соответствует этому свойству; удобно в Scala: require(a >= 0 && a < x).