Функциональное программирование похоже на написание ряда алгебраических уравнений, и поскольку алгебра не имеет 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
также можно использовать для необязательных параметров конструктора или метода
Contributors to this page:
Contents
- Введение
- Возможности Scala
- Почему Scala 3?
- Почувствуй Scala
- Пример 'Hello, World!'
- REPL
- Переменные и типы данных
- Структуры управления
- Моделирование данных
- Методы
- Функции первого класса
- Одноэлементные объекты
- Коллекции
- Контекстные абстракции
- Верхнеуровневые определения
- Обзор
- Первый взгляд на типы
- Интерполяция строк
- Структуры управления
- Моделирование предметной области
- Инструменты
- Моделирование ООП
- Моделирование ФП
- Методы
- Особенности методов
- Main методы в Scala 3
- Обзор
- Функции
- Анонимные функции
- Параметры функции
- Eta расширение
- Функции высшего порядка
- Собственный map
- Создание метода, возвращающего функцию
- Обзор
- Пакеты и импорт
- Коллекции в Scala
- Типы коллекций
- Методы в коллекциях
- Обзор
- Функциональное программирование
- Что такое функциональное программирование?
- Неизменяемые значения
- Чистые функции
- Функции — это значения
- Функциональная обработка ошибок
- Обзор
- Типы и система типов
- Определение типов
- Параметризованные типы
- Пересечение типов
- Объединение типов
- Алгебраические типы данных
- Вариантность
- Непрозрачные типы
- Структурные типы
- Зависимые типы функций
- Другие типы
- Контекстные абстракции
- Методы расширения
- Параметры контекста
- Контекстные границы
- Given импорты
- Классы типов
- Многостороннее равенство
- Неявное преобразование типов
- Обзор
- Параллелизм
- Scala утилиты
- Сборка и тестирование проектов Scala с помощью Sbt
- Рабочие листы
- Взаимодействие с Java