Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 787
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
20 .6 . Абстрактные типы 431
не зависит от них. Если есть побочные эффекты, то порядок инициализации становится значимым. И тогда могут возникнуть серьезные трудности в от
слеживании порядка запуска инициализационного кода, как было показано в предыдущем примере. Следовательно, ленивые val
переменные — идеаль
ное дополнение к функциональным объектам, в которых порядок инициа
лизации не имеет значения до тех пор, пока все в конечном счете не будет проинициализировано. А вот для преимущественно императивного кода эти переменные подходят меньше.
Ленивые функциональные языки
Scala — далеко не первый язык, использующий идеальную пару ленивых определений и функционального кода. Существует целая категория ленивых языков функционального программирования, в которых каждое значение и каждый параметр инициализиру
ются лениво. Яркий представитель этого класса языков — Haskell
[SPJ02].
20 .6 . Абстрактные типы
В начале этой главы в качестве объявления абстрактного типа мы показали код type
T
. Далее мы рассмотрим, что означает такое объявление абстрактно
го типа и для чего оно может пригодиться. Как и все остальные объявления абстракций, объявление абстрактного типа — заместитель для чеголибо, что будет конкретно определено в подклассах. В данном случае это тип, который будет определен ниже по иерархии классов. Следовательно, обозначение
T
ссылается на тип, который на момент его объявления еще неизвестен. Разные подклассы могут обеспечивать различные реализации
T
Рассмотрим широко известный пример, в который абстрактные типы впи
сываются вполне естественно. Предположим, что получена задача смодели
ровать привычный рацион животных. Начать можно с определения класса питания
Food и класса животных
Animal с методом питания eat
:
class Food abstract class Animal:
def eat(food: Food): Unit
Затем можно попробовать создать специализацию этих двух классов в виде класса коров
Cow
, питающихся травой
Grass
:
432 Глава 20 • Абстрактные члены class Grass extends Food class Cow extends Animal:
override def eat(food: Grass) = {} // Этот код не пройдет компиляцию
Но при попытке компиляции этих новых классов будут получены следу
ющие ошибки:
2 | class Cow extends Animal:
| ˆ
|class Cow needs to be abstract, since
|def eat(food: Food): Unit is not defined (Note that Food
|does not match Grass: class Grass is a subclass of class
|Food, but method parameter types must match exactly.)
3 | override def eat(food: Grass) = {} // This won't...
| ˆ
| method eat has a different signature than the
| overridden declaration
Дело в том, что метод eat в классе
Cow не переопределяет метод eat класса
Animal
, поскольку типы их параметров различаются: в классе
Cow это
Grass
, а в классе
Animal это
Food
Некоторые считают, что в отклонении этих двух классов виновата слиш
ком строгая система типов. Они говорят, что допустимо специализировать параметр метода в подклассе. Но если бы классы были позволены в том виде, в котором написаны, вы быстро оказались бы в весьма небезопасной ситуации.
К примеру, механизму проверки типов будет передан следующий скрипт:
class Food abstract class Animal:
def eat(food: Food): Unit class Grass extends Food class Cow extends Animal override def eat(food: Grass) = {} // Этот код не пройдет компиляцию,
// но если бы это случилось...
class Fish extends Food val bessy: Animal = new Cow bessy.eat(new Fish) // ...коров можно было бы накормить рыбой.
Если снять ограничения, то программа пройдет компиляцию, поскольку коровы из класса
Cow
— животные из класса
Animal
, а у класса
Animal есть метод кормления eat
, который принимает любую разновидность питания
Food
, включая рыбу, то есть
Fish
. Но коровы не едят рыбу!
20 .6 . Абстрактные типы 433
Вместо этого вам нужно применить более точное моделирование. Животные из класса
Animal потребляют (
eat
) питание
Food
, но какое именно питание потребляет каждое животное, зависит от самого животного. Это довольно четко можно выразить с помощью абстрактного типа, что и показано в ли
стинге 20.10.
Листинг 20.10. Моделирование подходящего питания с помощью абстрактных типов class Food abstract class Animal:
type SuitableFood <: Food def eat(food: SuitableFood): Unit
С новым определением класса животное
Animal может потреблять только то питание, которое ему подходит. Какое именно питание будет подходящим, нельзя определить на уровне класса
Animal
. Поэтому подходящее питание
SuitableFood моделируется в виде абстрактного типа. У него есть верхний ограничитель
Food
, что выражено условием
<:
Food
. Это значит, что любая конкретная реализация
SuitableFood
(в подклассе класса
Animal
) долж
на быть подклассом
Food
. К примеру, реализовать
SuitableFood классом
IOException не получится.
После определения
Animal можно, как показано в листинге 20.11, перейти к коровам. Класс
Cow устанавливает в качестве подходящего для коров пи
тания
SuitableFood траву
Grass
, а также определяет конкретный метод eat для данной разновидности питания.
Листинг 20.11. Реализация абстрактного типа в подклассе class Grass extends Food class Cow extends Animal:
type SuitableFood = Grass override def eat(food: Grass) = {}
Эти новые определения класса компилируются без ошибок. При попытке за
пустить с новыми определениями класс контрпримера про коров, которые едят рыбу (cowsthateatfish), будут получены следующие ошибки компиляции:
class Fish extends Food val bessy: Animal = new Cow scala> bessy.eat(new Fish)
1 |bessy.eat(new Fish)
| ˆˆˆˆˆˆˆˆ
| Found: Fish
| Required: bessy.SuitableFood
434 Глава 20 • Абстрактные члены
20 .7 . Типы, зависящие от пути
Еще раз посмотрим на последнее сообщение об ошибке. Нас интересует тип, требующийся для метода eat
: bessy.SuitableFood
. Указание типа состоит из ссылки на объект, bessy
, за которой следует поле типа объекта,
SuitableFood
Тем самым показывается, что объекты в Scala в качестве членов могут иметь типы. Смысл bessy.SuitableFood
— «тип
SuitableFood
, являющийся членом объекта, на который ссылается bessy»
, или, иначе, тип питания, подходящего для bessy
Тип вида bessy.SuitableFood называется типом, зависящим от пути
(pathdependent type). Слово «путь» здесь означает ссылку на объект.
Это может быть единственное имя, такое как bessy
, или более длинный путь доступа, такой как farm.barn.bessy
, где все составляющие, farm
, barn и bessy
, — переменные (или имена объектоводиночек), которые ссыла
ются на объекты.
Термин «тип, зависящий от пути» подразумевает, что тип зависит от пути; и в целом различные пути дают начало разным типам. Например, предпо
ложим, что для определения классов собачьего питания
DogFood и собак
Dog используется следующий код:
class DogFood extends Food class Dog extends Animal:
type SuitableFood = DogFood override def eat(food: DogFood) = {}
При попытке накормить собаку едой для коров ваш код не пройдет компи
ляцию:
val bessy = new Cow val lassie = new Dog scala> lassie.eat(new bessy.SuitableFood)
1 |lassie.eat(new bessy.SuitableFood)
| ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
| Found: Grass
| Required: DogFood
Проблема заключается в том, что типом объекта
SuitableFood
, переданного методу eat
, выступает bessy.SuitableFood
, а он несовместим с параметром типа eat
, которым является lassie.SuitableFood
В случае с двумя собаками из класса
Dog ситуация другая. Поскольку в клас
се
Dog тип
SuitableFood определен в качестве псевдонима для класса
DogFood
, то типы
SuitableFood двух представителей класса
Dog по факту одинаковы.
20 .7 . Типы, зависящие от пути 435
В результате экземпляр класса
Dog
, называемый lassie
, фактически может питаться тем, что подходит другому экземпляру класса
Dog
, который мы на
зовем bootsie
:
val bootsie = new Dog lassie.eat(new bootsie.SuitableFood)
Тип, зависящий от пути, напоминает синтаксис для типа внутреннего класса в Java, но есть существенное различие: в типе, зависящем от пути, называется внешний объект, а в типе внутреннего класса — внешний класс. Типы вну
тренних классов в стиле Java могут быть выражены и в Scala, но записываются подругому. Рассмотрим два класса — наружный
Outer и внутренний
Inner
:
class Outer:
class Inner
В Scala вместо применяемого в Java выражения
Outer.Inner к внутреннему классу обращаются с помощью выражения
Outer#Inner
. Синтаксис с исполь
зованием точки (
) зарезервирован для объектов. Представим, к примеру, что создаются экземпляры двух объектов типа
Outer
:
val o1 = new Outer val o2 = new Outer
Здесь o1.Inner и o2.Inner
— два типа, зависящих от пути, и это разные типы.
Оба они соответствуют более общему типу
Outer#Inner
(являются его под
типами), который представляет класс
Inner с произвольным внешним объ
ектом типа
Outer
. В отличие от этого тип o1.Inner ссылается на класс
Inner с конкретным внешним объектом, на который ссылается o1
. Точно так же тип o2.Inner ссыла ется на класс
Inner с другим конкретным внешним объектом, на который ссылается o2
В Scala, как и в Java, экземпляры внутреннего класса содержат ссылку на экземпляр охватывающего их внешнего класса. Это, к примеру, позволяет внутреннему классу обращаться к членам его внешнего класса. Таким обра
зом, невозможно создать экземпляр внутреннего класса, не имея какоголибо способа указать экземпляр внешнего класса. Один из способов заключается в создании экземпляра внутреннего класса внутри тела внешнего класса.
В подобном случае будет использован текущий экземпляр внешнего класса
(в ссылке на который можно задействовать this
).
Еще один способ заключается в использовании типа, зависящего от пути.
Например, в типе o1.Inner присутствует название конкретного внешнего объекта, поэтому можно создать его экземпляр:
new o1.Inner
436 Глава 20 • Абстрактные члены
Получившийся внутренний объект будет содержать ссылку на свой внеш
ний объект, то есть на объект, на который ссылается o1
. В отличие от этого, поскольку тип
Outer#Inner не содержит названия какоголибо конкретного экземпляра класса
Outer
, создать экземпляр данного класса невозможно:
scala> new Outer#Inner
1 |new Outer#Inner
| ˆˆˆˆˆˆˆˆˆˆˆ
| Outer is not a valid class prefix, since it is
| not an immutable path
20 .8 . Уточняющие типы
Когда класс является наследником другого класса, первый класс называют
номинальным подтипом другого класса. Этот подтип номинальный, поскольку у каждого типа есть имя и имена явным образом объявлены имеющими отно
шение подтипирования. Кроме того, в Scala дополнительно поддерживается
структурное подтипирование, где отношение подтипирования возникает просто потому, что у двух типов есть совместимые элементы. Для получе
ния в Scala структурного подтипирования нужно задействовать имеющиеся в данном языке уточняющие типы.
Обычно удобнее применять номинальное подтипирование, поэтому в любой новой конструкции нужно сначала попробовать воспользоваться именно им.
Имя — один краткий идентификатор, следовательно, короче явного пере
числения типов членов. Кроме того, структурное подтипирование зачастую более гибко, чем требуется. Тем не менее оно имеет свои преимущества. Одно из них заключается в том, что иногда действительно не нужно ничего опреде
лять в виде типов, кроме членов самого класса. Предположим, к примеру, что необходимо определить класс пастбища
Pasture
, который может содержать животных, поедающих траву. Одним из вариантов может быть определение трейта для животных, питающихся травой, —
AnimalThatEatsGrass
, и его примешивание в каждый класс, где он применяется. Но это будет слиш
ком многословным решением. В классе
Cow уже есть объявление, что это животное и оно ест траву, а теперь придется объявлять, что это животное, поедающее траву.
Вместо определения
AnimalThatEatsGrass можно воспользоваться уточня
ющим типом. Просто запишите основной тип,
Animal
, а за ним последова
тельность членов, перечисленных в фигурных скобках. Члены в фигурных скобках представляют дальнейшие указания, или, если хотите, уточнения, типов элементов из основного класса.
20 .9 . Практический пример: работа с валютой
1 ... 41 42 43 44 45 46 47 48 ... 64
437
Вот как записывается тип «животное, поедающее траву»:
Animal { type SuitableFood = Grass }
Теперь, имея в своем распоряжении этот тип, класс «пастбища» можно за
писать следующим образом:
class Pasture:
var animals: List[Animal { type SuitableFood = Grass }] = Nil
// ...
20 .9 . Практический пример: работа с валютой
Далее в главе рассмотрен практический пример, объясняющий порядок ис
пользования в Scala абстрактных типов. При этом будет поставлена задача разработать класс
Currency
. Обычный его экземпляр будет представлять денежную сумму в долларах, евро, йенах и некоторых других валютах.
Он позволит совершать с валютой ряд арифметических операций. Например, можно будет сложить две суммы в одной и той же валюте. Или умножить текущую сумму на коэффициент процентной ставки.
Эти соображения приводят к следующей первой конструкции класса валют:
// первая (нерабочая) конструкция класса Currency abstract class Currency:
val amount: Long def designation: String override def toString = s"$amount $designation"
def + (that: Currency): Currency = ...
def * (x: Double): Currency = ...
Поле amount
(сумма) в классе валют — количество представляемых ею валют
ных единиц. Оно имеет тип
Long
, то есть представляемая сумма денежных средств может быть очень крупной, сравнимой с рыночной капитализацией
Google или Apple. Здесь поле оставлено абстрактным в ожидании своего определения, когда в подклассе зайдет речь о конкретной сумме. Наимено
вание валюты designation
— строка, которая идентифицирует эту валюту.
Метод toString класса
Currency показывает сумму и наименование валюты.
Он будет выдавать результат следующего вида:
79 USD
11000 Yen
99 Euro
438 Глава 20 • Абстрактные члены
И наконец, имеются методы
+
для сложения сумм в валюте и
*
для умно
жения суммы в валюте на число с плавающей точкой. Конкретное значение в валюте можно создать, предоставив конкретные значения суммы и наи
менования валюты:
new Currency:
val amount = 79L
def designation = "USD"
Эта конструкция не вызовет нареканий, если задумано моделирование с ис
пользованием лишь одной валюты, например только долларов или только евро. Но она не будет работать при необходимости иметь дело сразу с не
сколькими валютами. Предположим, выполняется моделирование долларов и евро в качестве двух подклассов класса валюты:
abstract class Dollar extends Currency:
def designation = "USD"
abstract class Euro extends Currency:
def designation = "Euro"
На первый взгляд все выглядит вполне разумно. Но данный код позво
лит складывать доллары с евро. Результатом такого сложения будет тип
Currency
. Но это будет весьма забавная валюта — смесь евро и долларов. Вме
сто этого нужно получить более специализированную версию метода
+
. При его реализации в классе
Dollar он должен получать аргументы типа
Dollar и выдавать результат типа
Dollar
; при реализации в классе
Euro
— получать аргументы типа
Euro и выдавать результат типа
Euro
. Следовательно, тип метода сложения будет изменяться в зависимости от того, в каком классе находится. И все же хотелось бы создать метод сложения единожды, а не делать это при каждом новом определении валюты.
Чтобы помочь справиться с подобными ситуациями, Scala предоставляет весьма простую технологию. Если к моменту определения класса чтото еще неизвестно, то нужно сделать это «чтото» абстрактным. Технология применима как к значениям, так и к типам. В случае с валютами точные типы аргументов и результирующие типы метода сложения неизвестны, следовательно, являются подходящими кандидатами для того, чтобы стать абстрактными.
Это привело бы к следующей предварительной версии кода класса
AbstractCur rency
:
// вторая (все еще несовершенная) конструкция класса Currency abstract class AbstractCurrency: