Функциональная обработка ошибок

Функциональное программирование похоже на написание ряда алгебраических уравнений, и поскольку алгебра не имеет null значений или исключений, эти функции не используются в ФП. Это поднимает интересный вопрос: что делать в ситуациях, когда в коде ООП обычно используются null значение или исключение?

Решение Scala заключается в использовании конструкций, подобных классам Option/Some/None.

Примечание:

Первый пример

Хотя этот первый пример не имеет дело с null значениями, это хороший способ познакомиться с классами Option.

Представим, что нужно написать метод, который упрощает преобразование строк в целочисленные значения. И нужен элегантный способ обработки исключения, которое возникает, когда метод получает строку типа "Hello" вместо "1". Первое предположение о таком методе может выглядеть следующим образом:

def makeInt(s: String): Int =
  try
    Integer.parseInt(s.trim)
  catch
    case e: Exception => 0

Если преобразование работает, метод возвращает правильное значение Int, но в случае сбоя метод возвращает 0. Для некоторых целей это может быть хорошо, но не совсем точно. Например, метод мог получить "0", но мог также получить "foo", "bar" или бесконечное количество других строк, которые выдадут исключение. Это реальная проблема: как определить, когда метод действительно получил "0", а когда получил что-то еще? При таком подходе нет способа узнать правильный ответ наверняка.

Использование Option/Some/None

Распространенным решением этой проблемы в Scala является использование классов, известных как Option, Some и None. Классы Some и None являются подклассами Option, поэтому решение работает следующим образом:

Вот доработанная версия makeInt:

def makeInt(s: String): Option[Int] =
  try
    Some(Integer.parseInt(s.trim))
  catch
    case e: Exception => None

Этот код можно прочитать следующим образом: "Когда данная строка преобразуется в целое число, верните значение Int, заключенное в Some, например Some(1). Когда строка не может быть преобразована в целое число и генерируется исключение, метод возвращает значение None."

Эти примеры показывают, как работает makeInt:

val a = makeInt("1")  
// a: Option[Int] = Some(value = 1)  
val b = makeInt("one")
// b: Option[Int] = None

Как показано, строка "1" приводится к Some(1), а строка "one" - к None. В этом суть альтернативного подхода к обработке ошибок. Данная техника используется для того, чтобы методы могли возвращать значения вместо исключений. В других ситуациях значения Option также используются для замены null значений.

Примечание:

Потребитель makeInt

Теперь представим, что мы являемся потребителем метода makeInt. Известно, что он возвращает подкласс Option[Int], поэтому возникает вопрос: как работать с этими возвращаемыми типами?

Есть два распространенных ответа, в зависимости от потребностей:

Использование match выражений

Одним из возможных решений является использование выражения match:

makeInt(x) match
  case Some(i) => println(i)
  case None => println("That didn’t work.")

В этом примере, если x можно преобразовать в Int, вычисляется выражение в правой части первого предложения case; если x не может быть преобразован в Int, оценивается выражение в правой части второго предложения case.

Использование for выражений

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

val y = for
  a <- makeInt(stringA)
  b <- makeInt(stringB)
  c <- makeInt(stringC)
yield
  a + b + c

После выполнения этого выражения y может принять одно из двух значений:

Это можно проверить на примере:

val stringA = "1"
val stringB = "2"
val stringC = "3"
val y = for
  a <- makeInt(stringA)
  b <- makeInt(stringB)
  c <- makeInt(stringC)
yield
  a + b + c
// y: Option[Int] = Some(value = 6)  

Чтобы увидеть негативный кейс, достаточно изменить любую из строк на что-то, что нельзя преобразовать в целое число. В этом случае y равно None:

y: Option[Int] = None

Восприятие Option, как контейнера

Для лучшего восприятия Option, его можно представить как контейнер:

Если предпочтительнее думать об Option как о ящике, то None подобен пустому ящику. Что-то в нём могло быть, но нет.

Использование Option для замены null

Возвращаясь к значениям null, место, где null значение может незаметно проникнуть в код, — класс, подобный этому:

class Address(
  var street1: String,
  var street2: String,
  var city: String,
  var state: String,
  var zip: String
)

Хотя каждый адрес имеет значение street1, значение street2 не является обязательным. В результате полю street2 можно присвоить значение null:

val santa = Address(
  "1 Main Street",
  null,            
  "North Pole",
  "Alaska",
  "99705"
)

Исторически сложилось так, что в этой ситуации разработчики использовали пустые строки и значения null, оба из которых "взламывают" решения основной проблемы: street2 - необязательное поле. В Scala и других современных языках правильное решение состоит в том, чтобы заранее объявить, что street2 является необязательным:

class Address(
  var street1: String,
  var street2: Option[String],  
  var city: String,
  var state: String,
  var zip: String
)

Теперь можно написать более точный код:

val santa = Address(
  "1 Main Street",
  None,         
  "North Pole",
  "Alaska",
  "99705"
)

или так:

val santa = Address(
  "123 Main Street",
  Some("Apt. 2B"),
  "Talkeetna",
  "Alaska",
  "99676"
)

Option — не единственное решение

В этом разделе основное внимание уделялось Option классам, но у Scala есть несколько других альтернатив.

Например, три класса, известные как Try/Success/Failure, работают также, но (а) эти классы в основном используются, когда код может генерировать исключения, и (б) когда желательно использовать класс Failure, потому что он дает доступ к сообщению об исключении. Например, классы Try обычно используются при написании методов, которые взаимодействуют с файлами, базами данных или интернет-службами, поскольку эти функции могут легко создавать исключения.

Краткое ревью


Ссылки: