Главная страница

Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста


Скачать 6.24 Mb.
НазваниеОдерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
Дата27.04.2023
Размер6.24 Mb.
Формат файлаpdf
Имя файлаScala. Профессиональное программирование 2022.pdf
ТипДокументы
#1094967
страница40 из 64
1   ...   36   37   38   39   40   41   42   43   ...   64

$text

"
В данной сигнатуре четыре строки! Такой строчно-типизированный код с технической точки зрения является строго типизированным. Однако все, что находится здесь в поле зрения, относится к типу
String
, поэто­
му компилятор не может помочь вам отличить один элемент структуры от другого. Например, не сможет уберечь вас от следующего искажения структуры:
scala> title("chap:vcls", "bold", "Value Classes")
val res17: String =

chap:vcls


Код HTML нарушен. Предполагаемый для вывода на экран текст
Value
Classes используется в качестве класса стиля, в то время как для отобра­
жаемого текста chap.vcls предусматривалась роль гипертекстовой ссылки.
В довершение ко всему в качестве идентификатора такой ссылки выступила строка bold
, которая, в свою очередь, должна была выполнять роль класса стиля. Несмотря на всю череду ошибок, компилятор никак этому не вос­
противился.
Если определить для каждого понятия предметной области крошечный тип, то компилятор сможет принести больше пользы. Например, можно опреде­
лить собственный небольшой класс для стилей, идентификаторов гипертек­
стовых ссылок, отображаемого текста и кода HTML. Поскольку эти классы имеют один параметр и не имеют элементов, то могут быть определены как классы значений:
class Anchor(val value: String) extends AnyVal class Style(val value: String) extends AnyVal class Text(val value: String) extends AnyVal class Html(val value: String) extends AnyVal
Наличие этих классов позволяет создать версию title
, обладающую менее тривиальной сигнатурой типов наподобие такой:
def title(text: Text, anchor: Anchor, style: Style): Html =
Html(
s"" +

382 Глава 17 • Иерархия Scala s"

" +
text.value +
"

"
)
Теперь при попытке воспользоваться этой версией с аргументами, указанны­
ми в неверном порядке, компилятор сможет обнаружить ошибку, например:
scala> title(Anchor("chap:vcls"), Style("bold"),
Text("Value Classes"))
1 |title(new Anchor("chap:vcls"), new Style("bold"),
| ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
| Found: Anchor
| Required: Text
1 |title(Anchor("chap:vcls"), Style("bold"),
| ˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
| Found: Style
| Required: Anchor
2 | Text("Value Classes"))
| ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
| Found: Text
| Required: Style
17 .5 . Типы пересечений
Вы можете объединить два и более типа с помощью амперсанда (&), чтобы сформировать тип пересечения, например
Incrementing
&
Filtering
(При­
ращение и фильтрация). Вот пример использования классов и признаков, показанных в листингах 11.5, 11.6 и 11.9.
scala> val q = new BasicIntQueue with
Incrementing with Filtering val q: BasicIntQueue & Incrementing & Filtering = anon$...
Здесь q
инициализируется экземпляром анонимного класса, который расши­
ряет
BasicIntQueue и смешивает
Incrementing
(Приращение) с последующей
Filtering
(Фильтрация). Его выводимый тип,
BasicIntQueue
&
Incrementing
&
Filtering
, представляет собой тип пересечения, который указывает, что объект, на который ссылается q
, является экземпляром всех трех упомянутых типов:
BasicIntQueue
,
Incrementing и
Filtering
Тип пересечения является подтипом всех комбинаций составляющих его типов. Например, тип
B
&
I
&
F
является подтипом типов
B
,
I
,
F
,
B
&
I
,
B
&
F
,
I
&
F
и самого себя. Более того, поскольку типы пересечения являются коммута­
тивными, порядок появления типов в типе пересечения не имеет значения:

17 .6 . Типы объединения 383
например, тип
I
&
F
эквивалентен типу
F
&
I
. Следовательно,
B
&
I
&
F
также является подтипом
I
&
B
,
F
&
B
,
F
&
I
,
B
&
F
&
I
,
F
&
F
&
I
,
F
&
B
&
I
и т. д. Вот при­
мер, иллюстрирующий эти взаимосвязи между типами пересечений:
// Компилируется, так как B & I & F <: I & F
val q2: Incrementing & Filtering = q
// Компилируется, так как I & F эквивалентно F & I
val q3: Filtering & Incrementing = q2 17 .6 . Типы объединения
Scala предлагает дубликат для типов пересечения, называемых типами объ-
единения, которые состоят из двух или более типов, соединенных вертикаль­
ной чертой
(|)
, например
Plum
|
Apricot
. Тип объединения указывает, что объект является экземпляром по крайней мере одного из упомянутых типов.
Например, объект типа
Plum
|
Apricot является либо экземпляром
Plum
, либо экземпляром
Apricot
, либо и тем и другим
1
Как и типы пересечения, типы объединения являются коммутативными:
Plum
|
Apricot эквивалентно
Apricot
|
Plum
. В отличие от типов пересечения тип объединения является супертипом всех комбинаций входящих в него типов. Например,
Plum
|
Apricot является супертипом как
Plum
, так и
Apricot
Важно отметить, что
Plum
|
Apricot является не просто супертипом
Plum и
Apricot
, а их ближайшим общим супертипом, или наименьшей верхней
границей.
Добавление типов объединения и пересечения в Scala 3 гарантирует, что система типов Scala образует математическую решетку. Решетка — это частичный порядок, в котором любые два типа имеют как уникальную наи­
меньшую верхнюю границу, или LUB, так и уникальную наибольшую нижнюю
границу. В Scala 3 наименьшей верхней границей любых двух типов является их объединение, а наибольшей нижней границей — их пересечение. Напри­
мер, наименьшей верхней границей
Plum и
Apricot является
Plum
|
Apricot
Их наибольшая нижняя граница —
Plum
&
Apricot
Типы объединения имеют серьезные последствия для спецификации и реа­
лизации вывода типов и проверки типов в Scala. В то время как в Scala 2 ал­
горитм вывода типов должен был основываться на приближенном значении наименьшей верхней границы некоторых пар типов, фактическая наимень­
1
Вы можете произносить Plum | Apricot как Plum или Apricot.

384 Глава 17 • Иерархия Scala шая верхняя граница которых была пределом бесконечной серии, то в Scala
3 можно просто сформировать объединение этих типов.
Чтобы представить себе это, рассмотрим следующую иерархию:
trait Fruit trait Plum extends Fruit trait Apricot extends Fruit trait Pluot extends Plum, Apricot
Эти четыре типа образуют иерархию, показанную на рис. 17.2.
Fruit является супертипом как для
Plum
, так и для
Apricot
, но он не является ближайшим общим супертипом. Скорее, тип объединения
Plum
|
Apricot является бли­
жайшим общим супертипом, или наименьшей верхней границей, для
Plum и
Apricot
. Как показано на рис. 17.2, это означает, что тип объединения
Plum
|
Apricot является подтипом
Fruit
. И это действительно так, как показано на рисунке.
Рис. 17.2. Наименьшая верхняя и наибольшая нижняя границы val plumOrApricot: Plum | Apricot = new Plum {}
// Компилируется без проблем, так как Plum | Apricot <: Fruit val fruit: Fruit = plumOrApricot
// Нельзя использовать Fruit, так как нужен Plum | Apricot scala> val doesNotCompile: Plum | Apricot = fruit
1 |val doesNotCompile: Plum | Apricot = fruit
| ˆˆˆˆˆ
| Found: (fruit : Fruit)
| Required: Plum | Apricot
Двойной
Pluot является подтипом и для
Plum
, и для
Apricot
, но он не явля­
ется ближайшим общим подтипом. Скорее, тип пересечения
Plum
&
Apricot

17 .6 . Типы объединения 385
является ближайшим общим подтипом, или наибольшей нижней границей, для
Plum и
Apricot
. Из представленной на рис. 17.2 схемы следует, что тип пересечения
Plum
&
Apricot является супертипом
Pluot
. И это действительно так:
val pluot: Pluot = new Pluot {}
// Компилируется без проблем, так как Pluot <: Plum & Apricot val plumAndApricot: Plum & Apricot = pluot
// Нельзя использовать Plum & Apricot, так как нужен Pluot scala> val doesNotCompile: Pluot = plumAndApricot
1 |val doesNotCompile: Pluot = plumAndApricot
| ˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
| Found: (plumAndApricot : Plum & Apricot)
| Required: Pluot
Вы можете вызвать любой метод или получить доступ к любому полю, опре­
деленному в каждом из составляющих типов типа пересечения. Например, для экземпляра
Plum
&
Apricot вы можете вызывать любые методы, опреде­
ленные в
Plum или
Apricot
. В отличие от этого в типе объединения вы можете получить доступ только к тем элементам супертипов, которые являются об-
щими для составляющих типов. Таким образом, в экземпляре
Plum
|
Apricot вы можете получить доступ к членам
Fruit
(включая элементы, которые он наследует от
AnyRef и
Any
), но вы не можете получить доступ к каким­либо элементам, добавленным в
Plum или
Apricot
. Чтобы получить к ним доступ, вы должны выполнить сопоставление с образцом, чтобы определить реаль­
ный класс значения во время выполнения, например:
def errorMessage(msg: Int | String): String =
msg match case n: Int => s"Error number: ${n.abs}"
case s: String => s + "!"
Параметр msg метода errorMessage имеет тип
Int
|
String
. Поэтому вы можете напрямую вызывать в msg только методы, объявленные в
Any
, един­
ственном общем супертипе
Int и
String
. Вы не можете напрямую вызывать никакие другие методы, определенные либо в
Int
, либо в
String
. Чтобы получить доступ, например, к методу abs в
Int или оператору конкатенации строк (
+
) в
String
, необходимо выполнить сопоставление с образцом в msg
, как показано в теле метода errorMessage
. Вот несколько примеров исполь­
зования метода errorMessage
:
errorMessage("Oops") // "Oops!"
errorMessage(-42) // "Error number: 42"

386 Глава 17 • Иерархия Scala
17 .7 . Прозрачные трейты
У трейтов есть два основных применения: они позволяют определять клас­
сы с помощью композиции примешивания и определяют типы. В основном трейт используется как примешивание, а не как тип. Например, трейты
Incrementing и
Filtering из раздела 11.3 полезны в качестве примешиваний, однако они также имеют ограниченную ценность в качестве типов. По умол­
чанию можно выявить типы, определяемые этими трейтами. Например, компилятор Scala определит тип q
в следующей инструкции как тип пере­
сечения, в котором упоминаются и
Incrementing
, и
Filtering
:
scala> val q = new BasicIntQueue with
Incrementing with Filtering val q: BasicIntQueue & Incrementing & Filtering = anon$...
Вы можете указать, что не хотите, чтобы имя трейта отображалось в выводи­
мых типах, объявив его с помощью модификатора transparent
(прозрачный)
Например, объявив
Incrementing и
Filtering как прозрачные следующим образом:
transparent trait Incrementing extends IntQueue:
abstract override def put(x: Int) = super.put(x + 1)
transparent trait Filtering extends IntQueue:
abstract override def put(x: Int) =
if x >= 0 then super.put(x)
Теперь, когда трейты
Incrementing и
Filtering определены как прозрачные, их имена больше не будут отображаться в выводимых типах. Например, тип, выведенный из того же выражения создания экземпляра, показанного ранее, больше не будет упоминать
Incrementing или
Filtering
:
scala> val q = new BasicIntQueue with
Incrementing with Filtering val q: BasicIntQueue = anon$...
Модификатор transparent влияет только на вывод типов. Вы все еще можете использовать прозрачные трейты в качестве типов, если напишете их в явном виде. Вот пример, в котором прозрачные трейты
Incrementing и
Filtering отображаются в аннотации явного типа для переменной q
:
scala> val q: BasicIntQueue & Incrementing & Filtering =
new BasicIntQueue with Incrementing with Filtering val q: BasicIntQueue & Incrementing & Filtering = anon$...

Резюме 387
Помимо трейтов, явно помеченных как прозрачные, Scala 3 будет также счи­
тать прозрачными scala.Product
, java.lang.Serializable и java.lang.Com- parable
. Поскольку эти типы никогда не будут выводиться в Scala 3, когда вы захотите использовать их, вам придется делать это с помощью явных аннотаций типов или приписываний.
Резюме
В этой главе мы показали классы, находящиеся в верхней и нижней части иерархии классов Scala. Вы также увидели, как создавать свои собственные классы значений, в том числе как использовать их для «крошечных типов».
Вы узнали о типах пересечений и объединений и увидели, как они превра­
щают иерархию типов Scala в решетку. Наконец, вы узнали, как использовать модификатор transparent
, чтобы алгоритм вывода типов Scala не исполь­
зовал трейты, созданные в основном как примешивания, в качестве типов.
В следующей главе вы узнаете о параметризации типов.

18
Параметризация типов
В этой главе мы рассмотрим детали параметризации типов в Scala. Попутно продемонстрируем несколько техник сокрытия информации, представлен­
ных в главе 12, на конкретном примере: проектирования класса для чисто функцио нальных очередей.
Параметризация типов позволяет создавать обобщенные классы и трейты.
Например, множества имеют обобщенный характер и получают параметр типа: они определяются как
Set[T]
. В результате любой отдельно взятый эк­
земпляр множества может иметь тип
Set[String]
,
Set[Int]
и т. д., но должен быть множеством чего-либо. В отличие от языка Java, в котором разрешено использовать «сырые» типы (raw types), Scala требует указывать параметры типа. Вариантность определяет взаимоотношения наследования параме­
тризованных типов, к примеру, таких, при которых
Set[String]
является подтипом
Set[AnyRef]
Глава состоит из трех частей. В первой разрабатывается структура данных для чисто функциональных очередей. Во второй разрабатываются техно­
логические приемы сокрытия внутреннего представления деталей этой структуры. В третьей объясняется вариантность параметров типов и то, как она взаимодействует со сокрытием информации.
18 .1 . Функциональные очереди
Функциональная очередь представляет собой структуру данных с тремя операциями:
z z
head
— возвращает первый элемент очереди;
z z
tail
— возвращает очередь без первого элемента;

18 .1 . Функциональные очереди 389
z z
enqueue
— возвращает новую очередь с заданным элементом, добавлен­
ным в ее конец.
В отличие от изменяемой функциональная очередь не изменяет свое содер­
жимое при добавлении элемента. Вместо этого возвращается новая очередь, содержащая элемент. В данной главе наша цель — создать класс по имени
Queue
, работающий так:
val q = Queue(1, 2, 3) // Queue(1, 2, 3)
val q1 = q.enqueue(4) // Queue(1, 2, 3, 4)
q // Queue(1, 2, 3)
Будь у
Queue изменяемая реализация, операция enqueue в показанной ранее второй строке ввода повлияла бы на содержимое q
: по сути, после этой опе­
рации оба результата, и q1
, и исходная очередь q
, будут содержать последо­
вательность 1, 2, 3, 4. А для функциональной очереди добавленное значение обнаруживается только в результате q1
, но не в очереди q
, в отношении которой выполнялась операция.
Кроме того, чистые функциональные очереди слегка похожи на списки. Обе эти коллекции имеют так называемую абсолютно постоянную структуру данных, где старые версии остаются доступными даже после расширений или модификаций. Обе они поддерживают операции head и tail
. Но список растет, как правило, с начала с помощью операции
::
, а очередь — с конца с помощью операции enqueue
Как добиться эффективной реализации очереди? В идеале функциональная
(неизменяемая) очередь не должна иметь высоких издержек, существенно больших по сравнению с императивной (изменяемой) очередью. То есть все три операции: head
, tail и enqueue
— должны выполняться за постоянное время.
Одним из простых подходов к реализации функциональной очереди станет использование списка в качестве типа представления. Тогда head и tail
— просто аналогичные операции над списком, а enqueue
— конкатенация.
В таком варианте получится следующая реализация:
class SlowAppendQueue[T](elems: List[T]): // Неэффективное решение def head = elems.head def tail = new SlowAppendQueue(elems.tail)
def enqueue(x: T) = SlowAppendQueue(elems ::: List(x))
Проблемной в данной реализации является операция enqueue
. На нее уходит время, пропорциональное количеству элементов, хранящихся в очереди.
Если требуется постоянное время добавления, то можно также попробовать

390 Глава 18 • Параметризация типов изменить в списке, представляющем очередь, порядок следования элементов на обратный, чтобы последний добавляемый элемент стал в списке первым.
Тогда получится такая реализация:
class SlowHeadQueue[T](smele: List[T]): // Неэффективное решение
// smele — это реверсированный elems def head = smele.last def tail = new SlowHeadQueue(smele.init)
def enqueue(x: T) = SlowHeadQueue(x :: smele)
Теперь у операции enqueue постоянное время выполнения, а вот у head и tail
— нет. На их выполнение теперь уходит время, пропорциональное количеству элементов, хранящихся в очереди.
При изучении этих двух примеров реализация, в которой на все три опе­
рации будет затрачиваться постоянное время, не представляется такой уж простой. И действительно, возникают серьезные сомнения в возможности подобной реализации! Но, воспользовавшись сочетанием двух операций, можно подойти к желаемому результату очень близко. Замысел состоит в представлении очереди в виде двух списков: leading и trailing
. Список leading содержит элементы, которые располагаются от конца к началу, а элементы списка trailing следуют из начала в конец очереди, то есть в об­
ратном порядке. Содержимое всей очереди в любой момент времени равно коду leading
:::
trailing.reverse
Теперь, чтобы добавить элемент, следует просто провести конс­операцию в отношении списка trailing
, воспользовавшись оператором
::
, и тогда операция enqueue будет выполняться за постоянное время. Это значит, если изначально пустая очередь выстраивается на основе последовательно про­
веденных операций enqueue
, то список trailing будет расти, а список leading останется пустым. Затем перед выполнением первой операции head или tail в отношении пустого списка leading весь список trailing копируется в leading в обратном порядке следования элементов. Это делается с по­
мощью операции по имени mirror
. Реализация очередей с использованием данного подхода показана в листинге 18.1.
1   ...   36   37   38   39   40   41   42   43   ...   64


написать администратору сайта