Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 778
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
414 Глава 19 • Перечисления во всех его образцах
1
. Образцы перечисления в первую очередь нужны для предоставления фиксированного множества способов создания экземпляров типа перечисления.
19 .2 . Алгебраические типы данных
Алгебраический тип данных (algebraic data type, ADT) состоит из конечного набора образцов. Это естественный способ выражения моделей предметной области, позволяющий моделировать данные для каждого отдельного об
разца, который представляет один «конструктор данных» — определенный механизм создания экземпляра типа. В Scala запечатанное семейство case
классов составляет ADT — при условии, что как минимум один образец при
нимает параметры
2
. Например, вот тип ADT, описывающий три возможно
сти: ожидаемое значение («хороший» тип), ошибочное значение («плохой» тип) и исключение («злой» тип):
enum Eastwood[+G, +B]:
case Good(g: G)
case Bad(b: B)
case Ugly(ex: Throwable)
Как и в случае с EDT, вы не можете определять методы ни для каких кон
кретных образцов, будь то
Good
,
Bad или
Ugly
, но это можно сделать из общего суперкласса
Eastwood
. Вот пример метода map
, который преобразует значение
Good
, если
Eastwood является
Good
:
enum Eastwood[+G, +B]:
def map[G2](f: G => G2): Eastwood[G2, B] =
this match case Good(g) => Good(f(g))
case Bad(b) => Bad(b)
case Ugly(ex) => Ugly(ex)
case Good(g: G)
case Bad(b: B)
case Ugly(ex: Throwable)
А вот пример его использования:
1
Вы могли бы определять методы расширения для типов образцов, но в таких ситуа
циях, наверное, лучше вручную написать иерархию запечатанных классовобразцов.
2
Для сравнения, EDT — это запечатанное семейство классовобразцов, ни один об
разец которого не принимает параметры.
19 .2 . Алгебраические типы данных 415
val eastWood = Good(41)
eastWood.map(n => n + 1) // Good(42)
Реализация ADT и EDT немного отличается. Для каждого образца ADT, принимающего параметры, компилятор генерирует case
класс в объекте
компаньоне типа перечисления. Таким образом, для
Eastwood компилятор сгенерирует код, похожий на следующий:
// Сгенерированный запечатанный трейт ("тип перечисления")
sealed trait Eastwood[+G, +B]
object Eastwood: // Generated companion object
// Сгенерированные классы-образцы case class Good[+G, +B](g: G) extends Eastwood[G, B]
case class Bad[+G, +B](b: B) extends Eastwood[G, B]
case class Ugly[+G, +B](ex: Throwable) extends Eastwood[G, B]
Несмотря на то что итоговый тип фабричного метода, созданного case
классами, будет соответствовать типам отдельных классовэкземпляров, компилятор расширит последние до более общего типа перечисления. Вот несколько примеров:
scala> Good(42)
val res0: Eastwood[Int, Nothing] = Good(42)
scala> Bad("oops")
val res1: Eastwood[Nothing, String] = Bad(oops)
scala> Ugly(new Exception)
val res2: Eastwood[Nothing, Nothing] = Ugly(java.lang.Exception)
Если вам нужен более конкретный тип для своего образца, можете создать экземпляр с помощью new вместо фабричного метода. Например,
Good(1)
будет иметь тип
Eastwood[Int,
Nothing]
, однако у new
Good(1)
будет более конкретный тип,
Good[Int,
Nothing]
ADT могут быть рекурсивными. Например, образец может принимать тип перечисления в качестве параметра. Хорошим примером такого рекурсивного
ADT является связный список. Его можно определить в виде запечатанного типа с двумя подтипами: объектомодиночкой, представляющим пустой спи
сок, и классом
::
, который принимает два параметра — элемент (начало, или head
) и остальную часть списка (конец, или tail
). Ниже показан тип связного списка, в котором объект с пустым списком называется
Nada
, а класс
::
—
Yada
:
enum Seinfeld[+E]:
def ::[E2 >: E](o: E2): Seinfeld[E2] = Yada(o, this)
case Yada(head: E, tail: Seinfeld[E])
case Nada
416 Глава 19 • Перечисления
ADT
Seinfeld является рекурсивным типом, поскольку образец
Yada при
нимает другой тип
Seinfeld[E]
в качестве своего параметра tail
. Учитывая, что
Seinfeld объявляет метод
::
, вы можете создать экземпляр, который похож на
List из состава Scala, но начинается с
Nada
, а не с
Nil
:
scala> val xs = 1 :: 2 :: 3 :: Nada val xs: Seinfeld[Int] = Yada(1,Yada(2,Yada(3,Nada)))
19 .3 . Обобщенные ADT
Обобщенные алгебраические типы данных (generalized algebraic data types,
GADT) — это ADT, в которых запечатанный трейт принимает параметр типа, который заполняется образцами. Например:
enum Literal[T]:
case IntLit(value: Int) extends Literal[Int]
case LongLit(value: Long) extends Literal[Long]
case CharLit(value: Char) extends Literal[Char]
case FloatLit(value: Float) extends Literal[Float]
case DoubleLit(value: Double) extends Literal[Double]
case BooleanLit(value: Boolean) extends Literal[Boolean]
case StringLit(value: String) extends Literal[String]
Перечисление
Literal представляет GADT, поскольку оно принимает па
раметр типа
T
, который указывается каждым его образцом в инструкции extends
. Например, образец
IntLit уточняет
T
до
Int
, расширяя
Literal[Int]
Такого рода иерархия запечатанных типов носит специальное название
«обобщенные ADT», ввиду особых проблем, которые она создает для про
верки и вывода типов. Вот наглядный пример:
import Literal.*
def valueOfLiteral[T](lit: Literal[T]): T =
lit match case IntLit(n) => n case LongLit(m) => m case CharLit(c) => c case FloatLit(f) => f case DoubleLit(d) => d case BooleanLit(b) => b case StringLit(s) => s
Метод valueOfLiteral передает средство проверки типов, хотя ни один из его вариантов сопоставления не приводит к нужному итоговому типу
T
. На
пример, вариант case
IntLit(n)
выдает значение n
, которое имеет тип
Int
19 .4 . Что делает типы ADT алгебраическими 417
Проблема в том, что
Int не является ни типом
T
, ни его подтипом. Проверка этого типа происходит только лишь изза того, что, как замечает компилятор, для образца
IntList роль
T
может играть только
Int
. То же самое касается других вариантов. Кроме того, этот более конкретный тип передается об
ратно вызывающей стороне. Вот несколько примеров:
valueOfLiteral(BooleanLit(true)) // true: Boolean valueOfLiteral(IntLit(42)) // 42: Int
19 .4 . Что делает типы ADT алгебраическими
ADT называют алгебраическими, потому что они представляют собой при
менение алгебраической теории к типам. Эту связь с математикой можно наблюдать при сопоставлении каждого типа с его мощностью — количеством элементов, из которых он состоит. Если представить тип в виде множества значений, то его мощность будет равна мощности (количеству элементов) этого множества.
КРАТЧАЙШИЙ ПУТЬ
Этот раздел содержит материал о математических свойствах типов данных, которые можно определить с помощью enum . Если вы хотите вместо этого познакомиться с коллекциями Scala, можете переходить к следующей главе .
Например, у
Boolean есть два возможных значения, true и false
. Это два элемента, из которых состоит тип
Boolean
. Таким образом, мощность
Boolean составляет 2. У типа
Unit есть всего одно возможное значение — пустое мно
жество
()
, — поэтому его мощность — 1, а у типа
Nothing
— 0, так как он не содержит никаких элементов.
Вы можете найти или определить другие типы с мощностью 0, 1 или 2, од
нако
Nothing
,
Unit и
Boolean будет достаточно, чтобы проиллюстрировать алгебраические свойства. Что насчет типа мощностью 3? Если вам не при
ходит на ум очевидных вариантов из стандартной библиотеки, вы можете легко создать такой тип с помощью EDT:
enum TrafficLight:
case Red, Yellow, Green
У типа
TrafficLight есть три возможных значения:
Red
,
Yellow и
Green
, что делает его мощность равной 3.
Некоторые типы имеют очень большую мощность. Например, у типа
Byte есть 256 (2 8
) возможных значений в диапазоне от
Byte.MinValue до
1 ... 39 40 41 42 43 44 45 46 ... 64
418 Глава 19 • Перечисления
Byte.MaxValue включительно. Эти восьмибитные целочисленные значе
ния являются элементами, составляющими
Byte
, поэтому мощность этого типа равна 256. Тип
Int состоит из 2 32
элементов, что делает его мощность равной 2 32
, или 42 949 672 962. Многие типы, такие как
String
, имеют неограниченное множество возможных значений и, следовательно, бес
конечную мощность. Алгебра применима и к бесконечным мощностям, но для иллюстрации этих понятий я буду использовать типы с относительно небольшими, конечными мощностями.
Вы можете объединить несколько типов в новый, составной тип, сделав так, чтобы их мощности следовали закону сложения. Такой составной тип называется типом-суммой. В Scala кратчайшим способом определения типа
суммы является перечисление. Например:
enum Hope[+T]:
case Glad(o: T)
case Sad
Тип
Hope позволяет надеяться на лучшее, но готовиться к худшему. Он похож на тип
Option в Scala, где
Glad играет роль
Some
, а
Sad
—
None
. Из скольких элементов состоит
Hope[T]
? Поскольку это типсумма, мощность
Hope[T]
равна сумме мощностей составляющих его типов:
Glad[T]
и
Sad
Каждый экземпляр
Glad[T]
служит оберткой для типа
T
, поэтому мощ
ность
Glad[T]
равна мощности
T
. Например, мощность
Glad[Boolean]
– 2, как и
Boolean
. Составными элементами этого типа являются
Glad(true)
и
Glad(false)
Sad
— объектодиночка наподобие
Unit
, поэтому его мощ
ность составляет 1. Таким образом, мощность
Hope[Boolean]
равна 3
(
Glad[Boolean]
– 2 и
Sad
– 1). У этого типа есть три возможных экземпляра:
Glad(true)
,
Glad(false)
и
Sad
. В табл. 19.1 показаны другие примеры.
case class Both[A, B](a: A, b: B)
Таблица 19.1. Мощность Hope
Тип
Мощность
Составные элементы
Hope[Nothing]
0 + 1 = 1
Sad
Hope[Unit]
1 + 1 = 2
Glad(()), Sad
Hope[Boolean]
2 + 1 = 3
Glad(true), Glad(false), Sad
Hope[TrafficLight]
3 + 1 = 4
Glad(Red), Glad(Yellow), Glad(Green), Sad
Hope[Byte]
256 + 1 = 257
Glad(Byte.MinValue), …
Glad(Byte.MaxValue), Sad
19 .4 . Что делает типы ADT алгебраическими 419
Теперь вы знаете, как происходит сложение. Но что насчет умножения?
Аналогичным образом несколько типов можно объединить в один составной так, чтобы их мощности следовали закону умножения. Такой составной тип называют типом-произведением. В Scala кратчайшим способом определения типапроизведения является case
класс. Например: тип
Both позволяет со
четать два значения типов
A
и
B
, подобно тому как это делает тип
Tuple2
из состава Scala. Из скольких элементов состоит
Both[A,
B]
? Поскольку это типпроизведение, мощность
Both[A,
B]
равна произведению мощностей со
ставляющих его типов,
A
и
B
Чтобы перечислить все элементы
Both[A,
B]
, нужно взять сочетание каж
дого элемента типа
A
с каждым элементом типа
B
. Например,
TrafficLight и
Boolean имеют мощность 3 и 2 соответственно, поэтому мощность
Both[TrafficLight,
Boolean]
составит 3
× 2, или 6. В табл. 19.2 показано шесть возможных экземпляров этого типа вместе с некоторыми примерами.
Таблица 19.2. Мощность Both
Тип
Мощность
Составные элементы
Both[Nothing, Nothing]
0
× 0 = 0
Нет элементов
Both[Unit, Nothing]
1
× 0 = 0
Нет элементов
Both[Unit, Unit]
1
× 1 = 1
Both((), ())
Both[Boolean, Nothing]
2
× 0 = 0
Нет элементов
Both[Boolean, Unit]
2
× 1 = 2
Both(false, ()),
Both(true, ())
Both[Boolean, Boolean]
2
× 2 = 4
Both(false, false),
Both(false, true),
Both(true, false),
Both(true, true)
Both[TrafficLight, Nothing]
3
× 0 = 0
Нет элементов
Both[TrafficLight, Unit]
3
× 1 = 3
Both(Red, ()),
Both(Yellow, ()),
Both(Green, ())
Both[TrafficLight, Boolean]
3
× 2 = 6
Both(Red, false),
Both(Red, true),
Both(Yellow, false),
Both(Yellow, true),
Both(Green, false),
Both(Green, true)
420 Глава 19 • Перечисления
Тип
Мощность
Составные элементы
Both[TrafficLight, TrafficLight]
3
× 3 = 9
Both(Red, Red),
Both(Red, Yellow),
Both(Red, Green),
Both(Yellow, Red),
Both(Yellow, Yellow),
Both(Yellow, Green),
Both(Green, Red),
Both(Green, Yellow),
Both(Green, Green)
В целом, алгебраические типы данных представляют суммы произведений, где образцы формируют варианты типасуммы и каждый образец представляет типпроизведение, который может состоять из любого количества составных типов, начиная с нуля. EDT является частным случаем ADT, в котором каж
дый типпроизведение представлен объектомодиночкой.
За счет понимания алгебраических свойств своих структур данных вы можете использовать соответствующие математические законы, чтобы до
казать наличие определенных характеристик у своего кода. Например, что определенные процедуры рефакторинга сохранят логику вашей программы.
Показатели мощности ADT подчиняются законам сложения и вычитания, таким как тождество, коммутативность, ассоциативность и дистрибутив
ность. Функциональное программирование нередко предлагает возможность лучше понять свой код в контексте разных разделов математики.
Резюме
В этой главе вы познакомились с перечислениями в Scala — компактным механизмом определения иерархий запечатанных case
классов, формиру
ющих перечисляемые и алгебраические типы данных. Вы узнали, что в Scala
EDT и ADT находятся на разных концах одного спектра, и рассмотрели суть алгебраических типов. Конструкция enum в Scala делает распространенный подход к функциональному моделированию данных лаконичным и указы
вает на то, что EDT и ADT являются важными шаблонами проектирования.
Таблица 19.2 (окончание)
20
Абстрактные члены
Член класса или трейта называется абстрактным, если у него нет в классе полного определения. Реализовывать абстрактные элементы предполага
ется в подклассах того класса, в котором они объявлены. Воплощение этой идеи можно найти во многих объектноориентированных языках. Напри
мер, в Java можно объявить абстрактные методы. В Scala тоже, что было показано в разделе 10.2. Но Scala этим не ограничивается, и в нем данная идея реализуется самым универсальным образом: в качестве членов классов и трейтов можно объявлять не только методы, но и абстрактные поля и даже абстрактные типы.
В этой главе мы рассмотрим все четыре разновидности абстрактных членов: val
и var
переменные, методы и типы. Попутно изучим предварительно инициализированные поля, ленивые val
переменные и типы, зависящие от пути.
20 .1 . Краткий обзор абстрактных членов
В следующем трейте объявляется по одному абстрактному члену каждого вида: абстрактный тип (
T
), метод (
transform
), val
переменная (
initial
) и var
переменная (
current
):
trait Abstract:
type T
def transform(x: T): T
val initial: T
var current: T