Функциональная обработка ошибок
Info: JavaScript is currently disabled, code tabs will still work, but preferences will not be remembered.
Функциональное программирование похоже на написание ряда алгебраических уравнений, и поскольку алгебра не имеет null значений или исключений, они не используются и в ФП. Что поднимает интересный вопрос: как быть в ситуациях, в которых вы обычно используете null значение или исключение программируя в ООП стиле?
Решение Scala заключается в использовании конструкций, основанных на классах типа Option/Some/None.
Этот урок представляет собой введение в использование такого подхода.
Примечание:
- классы
SomeиNoneявляются подклассамиOption - вместо того чтобы многократно повторять "
Option/Some/None", следующий текст обычно просто ссылается на "Option" или на "классыOption"
Первый пример
Хотя этот первый пример не имеет дело с null значениями, это хороший способ познакомиться с классами Option.
Представим, что нужно написать метод, который упрощает преобразование строк в целочисленные значения.
И нужен элегантный способ обработки исключения, которое возникает,
когда метод получает строку типа "Hello" вместо "1".
Первое предположение о таком методе может выглядеть следующим образом:
def makeInt(s: String): Int =
try {
Integer.parseInt(s.trim)
} catch {
case e: Exception => 0
}
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возвращает типOption - если
makeIntполучает строку, которую он может преобразовать вInt, ответ помещается внутрьSome - если
makeIntполучает строку, которую не может преобразовать, то возвращаетNone
Вот доработанная версия makeInt:
def makeInt(s: String): Option[Int] =
try {
Some(Integer.parseInt(s.trim))
} catch {
case e: Exception => None
}
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") // Some(1)
val b = makeInt("one") // None
Как показано, строка "1" приводится к Some(1), а строка "one" - к None.
В этом суть альтернативного подхода к обработке ошибок.
Данная техника используется для того, чтобы методы могли возвращать значения вместо исключений.
В других ситуациях значения Option также используются для замены null значений.
Примечание:
- этот подход используется во всех классах библиотеки Scala, а также в сторонних библиотеках Scala.
- ключевым моментом примера является то, что функциональные методы не генерируют исключения;
вместо этого они возвращают такие значения, как
Option.
Потребитель makeInt
Теперь представим, что мы являемся потребителем метода makeInt.
Известно, что он возвращает подкласс Option[Int], поэтому возникает вопрос:
как работать с такими возвращаемыми типами?
Есть два распространенных ответа, в зависимости от потребностей:
- использование
matchвыражений - использование
forвыражений
Использование match выражений
Одним из возможных решений является использование выражения match:
makeInt(x) match {
case Some(i) => println(i)
case None => println("That didn’t work.")
}
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
}
val y = for
a <- makeInt(stringA)
b <- makeInt(stringB)
c <- makeInt(stringC)
yield
a + b + c
После выполнения этого выражения y может принять одно из двух значений:
- если все три строки конвертируются в значения
Int,yбудет равноSome[Int], т.е. целым числом, обернутым внутриSome - если какая-либо из трех строк не может быть преобразована в
Int,yравенNone
Это можно проверить на примере:
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
}
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 примет значение Some(6).
Чтобы увидеть негативный кейс, достаточно изменить любую из строк на что-то, что нельзя преобразовать в целое число.
В этом случае y равно None:
y: Option[Int] = None
Восприятие Option, как контейнера
Для лучшего восприятия Option, его можно представить как контейнер:
Someпредставляет собой контейнер с одним элементомNoneне является контейнером, в нем ничего нет
Если предпочтительнее думать об 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 = new Address(
"1 Main Street",
null, // <-- О! Значение null!
"North Pole",
"Alaska",
"99705"
)
val santa = Address(
"1 Main Street",
null, // <-- О! Значение 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 = new Address(
"1 Main Street",
None, // 'street2' не имеет значения
"North Pole",
"Alaska",
"99705"
)
val santa = Address(
"1 Main Street",
None, // 'street2' не имеет значения
"North Pole",
"Alaska",
"99705"
)
или так:
val santa = new Address(
"123 Main Street",
Some("Apt. 2B"),
"Talkeetna",
"Alaska",
"99676"
)
val santa = Address(
"123 Main Street",
Some("Apt. 2B"),
"Talkeetna",
"Alaska",
"99676"
)
Option — не единственное решение
В этом разделе основное внимание уделялось Option классам, но у Scala есть несколько других альтернатив.
Например, три класса, известные как Try/Success/Failure, работают также,
но (а) эти классы в основном используются, когда код может генерировать исключения,
и (б) когда желательно использовать класс Failure, потому что он дает доступ к сообщению об исключении.
Например, классы Try обычно используются при написании методов, которые взаимодействуют с файлами,
базами данных или интернет-службами, поскольку эти функции могут легко создавать исключения.
Краткое ревью
Этот раздел был довольно большим, поэтому давайте подведем краткое ревью:
- функциональные программисты не используют
nullзначения - основной заменой
nullзначениям является использование классовOption - функциональные методы не выдают исключений; вместо этого они возвращают такие значения, как
Option,TryилиEither - распространенными способами работы со значениями
Optionявляются выраженияmatchиfor Optionможно рассматривать как контейнеры с одним элементом (Some) и без элементов (None)Optionтакже можно использовать для необязательных параметров конструктора или метода