Функциональная обработка ошибок
Функциональное программирование похоже на написание ряда алгебраических уравнений,
и поскольку алгебра не имеет 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
Если преобразование работает, метод возвращает правильное значение 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
Этот код можно прочитать следующим образом:
"Когда данная строка преобразуется в целое число, верните значение 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
значений.
Примечание:
- этот подход используется во всех классах библиотеки 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.")
В этом примере, если 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
может принять одно из двух значений:
- если все три строки конвертируются в значения
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
// y: Option[Int] = Some(value = 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 = 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
обычно используются при написании методов,
которые взаимодействуют с файлами, базами данных или интернет-службами,
поскольку эти функции могут легко создавать исключения.
Краткое ревью
- функциональные программисты не используют
null
значения - основной заменой
null
значениям является использование классовOption
- функциональные методы не выдают исключений; вместо этого они возвращают такие значения, как
Option
,Try
илиEither
- распространенными способами работы со значениями
Option
являются выраженияmatch
иfor
Option
можно рассматривать как контейнеры с одним элементом (Some
) и без элементов (None
)Option
также можно использовать для дополнительных параметров конструктора или метода
Ссылки:
- Scala3 book
- I hate NULL and all its variants!
- Making something out of nothing (or, why None is better than NaN and NULL)
- Maybe in Java
- Null, NullPointerException and dealing with it
- Null References: The Billion Dollar Mistake
- SKB – Scala Either
- SKB – Scala Option
- SKB – Scala Option map
- SKB – Scala Try