Выбрасывая исключения в Scala, что такое "официальное правило",

Я следую курсу Scala на Coursera. Я начал читать книгу Scala Одерского.

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

Но в книге и курсе Мартин Одерский, кажется, не говорит (по крайней мере на данный момент), что исключения плохие, и он использует их много. Я также заметил, что методы assert/требуют...

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

Может кто-нибудь объяснить мне, что я должен использовать в этом случае?

Ответ 1

Основным направлением является использование исключений для чего-то действительно исключительного **. Для "обычного" сбоя гораздо лучше использовать Option или Either. Если вы взаимодействуете с Java, где исключаются исключения, когда кто-то чихает неправильно, вы можете использовать Try, чтобы оставаться в безопасности.

Возьмем несколько примеров.

Предположим, что у вас есть метод, который извлекает что-то с карты. Что может пойти не так? Ну, что-то драматическое и опасное, как переполнение стека segfault *, или что-то ожидаемое, подобное элементу, не найдено. Вы позволили бы переполнению стека segfault вызвать исключение, но если вы просто не найдете элемент, почему бы не вернуть Option[V] вместо значения или исключения (или null),

Теперь предположим, что вы пишете программу, в которой пользователь должен ввести имя файла. Теперь, если вы не собираетесь немедленно заручиться программой, когда что-то пойдет не так, Either - это путь:

def main(args: Array[String]) {
  val f = {
    if (args.length < 1) Left("No filename given")
    else {
      val file = new File(args(0))
      if (!file.exists) Left("File does not exist: "+args(0))
      else Right(file)
    }
  }
  // ...
}

Теперь предположим, что вы хотите проанализировать строку с номерами, разделенными пробелами.

val numbers = "1 2 3 fish 5 6"      // Uh-oh
// numbers.split(" ").map(_.toInt)  <- will throw exception!
val tried = numbers.split(" ").map(s => Try(s.toInt))  // Caught it!
val good = tried.collect{ case Success(n) => n }

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

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

* Изменить: Segfaults разбивают JVM и никогда не должны происходить независимо от байт-кода; даже исключение вам тогда не поможет. Я имел в виду переполнение стека.

** Изменить: Исключения (без трассировки стека) также используются для потока управления в Scala - они на самом деле довольно эффективный механизм, и они включают такие вещи, как установленные библиотекой выражения break и return, который возвращается из вашего метода, даже если элемент управления фактически перешел в одно или несколько закрытий. В основном, вы не должны беспокоиться об этом сами, за исключением того, что поймать все Throwable не является такой супер-идеей, так как вы можете ошибиться по одному из этих исключений потока управления.

Ответ 2

Итак, это одно из тех мест, где Scala специально отделяет функциональную чистоту от простоты перехода от/совместимости - с устаревшими языками и средами, в частности с Java. Функциональная чистота нарушается исключениями, поскольку они нарушают ссылочную целостность и делают невозможным разумное рассуждение. (Конечно, не заканчивающиеся рекурсии делают то же самое, но несколько языков готовы обеспечить соблюдение ограничений, которые сделают невозможными). Чтобы сохранить функциональную чистоту, вы используете Option/Maybe/Lither/Try/Validation, все из которых кодируют успех или отказ в качестве ссылочно-прозрачного типа, и использовать различные функции более высокого порядка, которые они предоставляют, или основной синтаксис синтаксических языков, чтобы сделать вещи более ясными. Или, в Scala, вы можете просто решить, чтобы отличить функциональную чистоту, зная, что это может облегчить ситуацию в краткосрочной перспективе, но сложнее в долгосрочной перспективе. Это похоже на использование "null" в Scala, или изменяемые коллекции, или локальные "var" s. Мягко стыдно, и не делайте этого, но все в крайнем случае.