Появился в Scala 2.10.0 и был портирован в Scala 2.9.3.
Как и в других языках Scala позволяет производить выбрасывание исключений:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
case class Customer(age: Int) class Cigarettes case class UnderAgeException(message: String) extends Exception(message) def buyCigarettes(customer: Customer): Cigarettes = if (customer.age < 16) throw UnderAgeException(s"Customer must be older than 16 but was ${customer.age}") else new Cigarettes |
Выброшенные исключения могут быть отловлены и обработаны с помощью блоков Try/Catch содержащих частично применяемые функции, которые указывают на типы, исключений, которые мы хотим обработать. Кроме того, в scala блок Try/Catch – это выражение которое может иметь результат.
Например, так:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
val youngCustomer = Customer(15) try { buyCigarettes(youngCustomer) "Yo, here are your cancer sticks! Happy smokin'!" } catch { case UnderAgeException(msg) => msg } |
Обработка ошибок функциональным способом
Обработка ошибок классическим способом плохо сочетается с функциональным программированием, такой подход также очень плохо сочетается с параллелизмом. Например, в системе Акторов, если исключение происходит в другом акторе, то это означает, что Вы не можете поймать это исключение, так как оно находиться в другом потоке выполнения, но информация об ошибке не должна теряться.
Для решения таких задач в Scala предусмотрена возможность возвращать значение соответствующе ошибке из функций. Это достигается применением типа Try включенного в состав Scala. Кроме того, существует более обобщенный тип Either.
Семантика типа Try
Работа с типом Try аналогична на подход работы с типом Option. Тип Option[A] является контейнером для типа A и может как содержать значение, так и нет. Так и по аналогии тип Try[A] представляет результат вычисления типа A, который может быть успешно вычислен или в результате вычисления могла произойти ошибка Throwable. При этом экземпляры такого контейнера могут быть спокойно переброшены между двумя конкурирующими потоками выполнения.
Существует два вида Try. Первый это экземпляр Try[A] представляющий результат успешного вычисления функций без возникновения ошибок представленный экземпляром Success[A] который является контейнером для типа A. Второй вариант, возникает в случае возникновения ошибки и является экземпляром класса Failure[A] который является контейнером для Throwable и служит для отображения исключений и других видов ошибок.
В том случае, если Вы заранее знаете, что в результате вычисления функции могут возникнуть ошибки вы можете просто использовать тип в качестве возвращаемого значения Try[A]. Что в свою очередь потребует от пользователей данной функции обработать ошибку соответствующим образом.
Для примера рассмотрим случай написание программы скачивания веб страниц. По заданию на вход программы передаться url страницы которую мы хотим скачать. Частью нашего приложения будет функция которая разбирает текстовое представление url и возвращает объект ava.net.URL построенный из него:
import scala.util.Try
import java.net.URL
def parseURL(url: String): Try[URL] = Try(new URL(url))
Как Вы видите результатом функции является экземпляр Try[URL]. Если переданный в программу url синтаксически правильный, то результатом будет Success[URL]. Если же конструктор класса URL не сможет разобрать строку, то он выбросит исключение MalformedURLException и в этом случае результатом будет Failure[URL].
Такое поведение достигается благодаря фабричному методу apply объекта компаньона Try. Данный метод принимает параметр, передаваемый по имени типа A (в нашем случае URL). Для примера это означает, что вызов new URL(url) будет произведен внутри метода apply объекта Try. Внутри метода не фатальные исключения возвращаться обернутые в Failure.
То есть parseURL(“http://danielwestheide.com”) вернет Success[URL] содержащий вложенные объект URL, parseURL(“garbage”) вернет Failure[URL] содержащий внутри себя исключение MalformedURLException.
Работа с Try значениями
Работа с типом Try аналогична работе с типом Option.
Проверить, что Try завершился успешно можно при помощи метода isSuccess а получить значение вычислений методом get.
Также присутствует метод getOrElse который позволяет присвоить Try значение по умолчанию, которое используется если результат Failure:
val url = parseURL(Console.readLine(“URL: “)) getOrElse new URL(“http://duckduckgo.com”)
Если пользователь введет не корректный url то будет использоваться http://duckduckgo.com
Объединение Try в цепочки
Как и в случае с типом Option тип Try поддерживает все основные операции коллекций, что позволяет объединять операции с Try в цепочки и получать любые исключения которые происходят в более читаемой форме.
Операции map и flatMap
Преобразование (функцией map) Try[A] который содержит Success[A] в значение Try[B] получит результат Success[B]. Если при этом имеем Failure[A] то преобразование в Try[B] даст результат Failure[B] и с другой стороны будет содержать тоже исключение что и Failure[A].
parseURL(“http://danielwestheide.com”).map(_.getProtocol)
// results in Success(“http”)
parseURL(“garbage”).map(_.getProtocol)
// results in Failure(java.net.MalformedURLException: no protocol: garbage)
Если произвести вызов map на нескольких вложенных Try мы получим не очень подходящий результат. Например, получение потока ввода из URL:
import java.io.InputStream
def inputStreamForURL(url: String): Try[Try[Try[InputStream]]] = parseURL(url).map { u =>
Try(u.openConnection()).map(conn => Try(conn.getInputStream))
}
Анонимная функция, передаваемая в два вызова map и при этом каждая, возвращает Try в результате даст тип Try[Try[Try[InputStream]]].
Для обхода такого поведения служит функция flatMap, которая ожидает, что ей будет передана функция, которая получает A и возвращает Try[B]. Если Try[A] уже несет в себе исключение Failure[A] то это исключение Failure[B] просто оборачивая исключение по цепочке если значение Try[A] это Success[A] то функция flatMap распаковывает A и передает значение преобразующей функции которая должна и возвращает Try[B].
Что означает возможность создания каналов операций над экземпляром Success путем объединения вызовов flatMap , если хотя бы в одном элементе возникнет ошибка то и результатом все цепочки также станет ошибка.
Перепишем пример:
def inputStreamForURL(url: String): Try[InputStream] = parseURL(url).flatMap { u =>
Try(u.openConnection()).flatMap(conn => Try(conn.getInputStream))
}
Сейчас результатом работы функции будет Try[InputStream] который может иметь значения Failure в исключением внутри него, если случилась ошибка или Success с завернутым в него InputStream если операция выполнилась успешно.
Методы filter foreach
Также, как и в случае типом Option для типа Try допустимо использовать операции filter и foreach.
Метод filter возвращает Failure если экземпляр Try на котором он был вызван уже содержит результат Failure или если вычисленный предикат false (в этом случае обернутым исключением будет NoSuchElementException). Если Try на котором произведен вызов возвратит Success и предикат переданный вернет true то результатом выполнения метода будет не измененный Succcess.
def parseHttpURL(url: String) = parseURL(url).filter(_.getProtocol == “http”)
parseHttpURL(“http://apache.openmirror.de”) // results in a Success[URL]
parseHttpURL(“ftp://mirror.netcologne.de/apache.org”) // results in a Failure[URL]
Функция переданная в foreach выполняется только если Try имеет состояние Success, что позволяет использовать ее для создания «побочных» эффектов. В этом ключе функция передання в метод foreach выполнится единожды и получит на вход значение обернутое в Success.
parseHttpURL(“http://danielwestheide.com”).foreach(println)
Выражения цикла for
Поддержка методов flatMap, map и filter означает возможность использовать синтаксис оператора for для обработки цепочек Try экземпляров. Обычно, такой подход позволяет получать более читаемый код. Для демонстрации такого подхода рассмотрим пример, реализуем метод, который получает содержимое веб страницы по троке url, при этом используем синтаксис for:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import scala.io.Source def getURLContent(url: String): Try[Iterator[String]] = for { url <- parseURL(url) connection <- Try(url.openConnection()) is <- Try(connection.getInputStream) source = Source.fromInputStream(is) } yield source.getLines() |
В данном примере есть три места в которых, что-то может пойти не так и во всех них используется тип Try. Первым таким местом является функция parseURL которая возвращает Try[URL]. При этом работа идет дальше, только если ее результат Success[URL] далее мы пытаемся открыть соединение и получить поток на чтение для этого url. Если все эти операции прошли успешно, то мы создаем набор строк, прочитанных из потока для чтения. Таким образом мы эффективно соединили цепочку вызовов flatMap в стиле for цикла и результатом выполнения будет плоский список Try[Iterator[String]].
Однако стоит заметить, что данный пример не совершенен так как мы забыли закрыть поток. Более того задачу можно решить проще используя Source#fromURL.
Сравнение с шаблоном
В некоторой точке кода вполне может возникнуть ситуация, когда у вас есть экземпляр Try и вам необходимо выполнить некоторый код в зависимости от результата. Для этих целей можно использовать сравнение с шаблоном, это легко сделать так как Success и Failure это case классы.
Если нам необходимо создать страницу ответа на запрос или напечатать сообщение об ошибке, это можно сделать так:
1 2 3 4 5 6 7 8 9 10 11 |
import scala.util.Success import scala.util.Failure getURLContent("http://danielwestheide.com/foobar") match { case Success(lines) => lines.foreach(println) case Failure(ex) => println(s"Problem rendering URL content: ${ex.getMessage}") } |
Восстановление из состояния ошибки
Если Вы хотите предоставить какое то действие по умолчанию в случае шибки (то есть получения экземпляра Failure), то Вам нет необходимости использовать метод getOrElse, уме есть хорошая альтернатива метод recover, который ожидает получить на вход частично определенную функцию которая возвращает другой Try. Если метод recover вызывается на Success то это же значение и возвращается в результате. В противном случае вызывается частично применяемая функция, которой на вход передается Failure и ее результат возвращается как Success. Например, напечатаем различные сообщения об ошибках:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import java.net.MalformedURLException import java.io.FileNotFoundException val content = getURLContent("garbage") recover { case e: FileNotFoundException => Iterator("Requested page does not exist") case e: MalformedURLException => Iterator("Please make sure to enter a valid URL") case _ => Iterator("An unexpected error has occurred. We are so sorry!") } |
В этом случае мы может постоянно использовать метод get который мы всегда будет вызывать на Try[Iterator[String]] который мы присвоили константе content, потому что мы точно знаем, что результат Success. Вызов content.get.foreach(println) всегда напечатает строку результат.
Общая информация
Идея обработки ошибок в Scala отличается от принятых в java подходов, применения типа Try позволяет инкапсулировать (изолировать) вычисления который могут приводить к ошибкам и производить соединение цепочек вычислений в элегантной и простой форме. Вы можете перенести свои знания и навыки работы с коллекциями и типом Option для работы с исключениями и производить кодирование в единой манере. Как и тип Option тип Try поддерживает метод orElse. Кроме того, есть еще методы transform и recoverWith.