Главная страница
Навигация по странице:

  • Листинг 20.6.

  • Листинг 20.7.

  • Листинг 20.9.

  • Ленивые функциональные языки

  • Листинг 20.10.

  • Листинг 20.11.

  • 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
    страница45 из 64
    1   ...   41   42   43   44   45   46   47   48   ...   64
    Листинг 20.5. Трейт, принимающий параметрические поля trait RationalTrait(val numerArg: Int, val denomArg: Int):
    require(denomArg != 0)
    private val g = gcd(numerArg, denomArg)
    val numer = numerArg / g val denom = denomArg / g private def gcd(a: Int, b: Int): Int =
    if (b == 0) a else gcd(b, a % b)
    override def toString = s"$numer/$denom"
    Листинг 20.6. Параметрические поля трейта в выражении анонимного класса scala> new RationalTrait(1 * x, 2 * x) {}
    val res1: RationalTrait = 1/2
    Сфера применения параметрических полейне ограничивается анонимными классами, они могут использоваться также в объектах или именованных под­
    классах. Соответствующие примеры показаны в листингах 20.7 и 20.8. Класс
    RationalClass
    , показанный в листинге 20.8, иллюстрирует общую схему до­
    ступности параметров класса для инициализации супертрейта.
    Листинг 20.7. Параметрические поля трейта в определении объекта object TwoThirds extends RationalTrait(2, 3)
    Листинг 20.8. Параметрические поля трейта в определении класса class RationalClass(n: Int, d: Int) extends RationalTrait(n, d):
    def + (that: RationalClass) = new RationalClass(
    numer * that.denom + that.numer * denom,
    denom * that.denom
    )

    428 Глава 20 • Абстрактные члены
    Ленивые val-переменные
    Параметры трейта могут применяться для точной имитации поведения ини­
    циализации аргументов конструктора класса. Но иногда лучше позволить самой системе разобраться, как и что должно быть проинициализировано.
    Добиться этого можно с помощью определения ленивой val
    ­переменной.
    Если перед определением val
    ­переменной поставить модификатор lazy
    , то выражение инициализации справа будет вычисляться только при первом использовании val
    ­переменной.
    Определим, к примеру, объект
    Demo с val
    ­переменной:
    object Demo:
    val x = { println("initializing x"); "done" }
    Теперь сначала сошлемся на
    Demo
    , а затем на
    Demo.x
    :
    scala> Demo initializing x val res0: Demo.type = Demo$@3002e397
    scala> Demo.x val res1: String = done
    Как видите, на момент использования объекта
    Demo его поле x
    становится проинициализированным. Инициализация x
    составляет часть инициализа­
    ции
    Demo
    . Но ситуация изменится, если определить поле x
    как lazy
    :
    object Demo:
    lazy val x = { println("initializing x"); "done" }
    scala> Demo val res2: Demo.type = Demo$@24e5389c scala> Demo.x initializing x val res3: String = done
    Теперь инициализация
    Demo не включает инициализацию x
    . Она будет отложена до первого использования x
    . Это похоже на ситуацию опреде­
    ления x
    в качестве метода без параметров с помощью ключевого слова def
    . Но в отличие от def ленивая val
    ­переменная никогда не вычисляется более одного раза. Фактически после первого вычисления ленивой val
    ­
    переменной результат вычисления сохраняется, чтобы его можно было применить повторно при последующем использовании той же самой val
    ­
    переменной.

    20 .5 . Инициализация абстрактных val-переменных 429
    При изучении данного примера создается впечатление, что объекты, подоб­
    ные
    Demo
    , сами ведут себя как ленивые val
    ­переменные, поскольку инициа­
    лизируются по необходимости при их первом использовании. Так и есть.
    Действительно, определение объекта может рассматриваться как сокращен­
    ная запись для определения ленивой val
    ­переменной с анонимным классом, в котором описывается содержимое объекта.
    Используя ленивые val
    ­переменные, можно переделать
    RationalTrait
    , как показано в листинге 20.9. В новом определении трейта все конкретные поля определены как lazy
    . Есть еще одно изменение, касающееся предыдущего определения
    RationalTrait
    , показанного выше, в листинге 20.4. Данное из­
    менение заключается в том, что условие require было перемещено из тела трейта в инициализатор приватного поля g
    , вычисляющий наибольший об­
    щий делитель для numerArg и denomArg
    . После внесения этих изменений при инициализации
    LazyRationalTrait делать больше ничего не нужно, посколь­
    ку весь код инициализации теперь является правосторонней частью ленивой val
    ­переменной. Таким образом, теперь вполне безопасно инициализировать абстрактные поля
    LazyRationalTrait после того, как класс уже определен.
    Давайте рассмотрим пример:
    scala> val x = 2
    val x: Int = 2
    Листинг 20.9. Инициализация трейта с ленивыми val-переменными trait LazyRationalTrait:
    val numerArg: Int val denomArg: Int lazy val numer = numerArg / g lazy val denom = denomArg / g override def toString = s"$numer/$denom"
    private lazy val g =
    require(denomArg != 0)
    gcd(numerArg, denomArg)
    private def gcd(a: Int, b: Int): Int =
    if b == 0 then a else gcd(b, a % b)
    Рассмотрим пример:
    scala> new LazyRationalTrait:
    val numerArg = 1 * x val denomArg = 2 * x val res4: LazyRationalTrait = 1/2

    430 Глава 20 • Абстрактные члены
    Здесь не нужны какие­либо предварительные вычисления. Проследим по­
    следовательность инициализации, приводящей к тому, что в показанном ранее коде на стандартное устройство будет выведена строка
    1/2 1. Создается новый экземпляр
    LazyRationalTrait
    , и запускается код инициа лизации
    LazyRationalTrait
    . Этот код пуст — ни одно из полей
    LazyRa tionalTrait еще не проинициализировано.
    2. С помощью вычисления выражения new определяется первичный кон­
    структор анонимного подкласса. Данная процедура включает в себя ини­
    циализацию numerArg значением
    2
    и инициализацию denomArg значением
    4 3. Далее интерпретатор в отношении создаваемого объекта вызывает ме­
    тод toString
    , чтобы получившееся значение можно было бы вывести на стандартное устройство.
    4. Метод toString
    , определенный в трейте
    LazyRationalTrait
    , выполняет первое обращение к полю numer
    , что вызывает вычисление инициализа­
    тора.
    5. Инициализатор поля numer обращается к приватному полю g
    ; таким образом, следующим вычисляется g
    . При этом вычислении происходит обращение к numerArg и denomArg
    , которые были определены в шаге 2.
    6. Метод toString обращается к значению denom
    , что вызывает вычисление denom
    . При этом происходит обращение к значениям denomArg и g
    . Ини­
    циализатор поля g
    заново уже не вычисляется, поскольку был вычислен в шаге 5.
    7. Создается и выводится строка результата
    1/2
    Обратите внимание: в классе
    LazyRationalTrait определение g
    появляется в тексте кода после определений в нем numer и denom
    . Несмотря на это, вви­
    ду того что все три значения ленивые, g
    инициализируется до завершения инициализации numer и denom
    Тем самым демонстрируется важное свойство ленивых val
    ­переменных: по­
    рядок следования их определений в тексте кода не играет никакой роли, по­
    скольку инициализация значений выполняется по требованию. Стало быть, ленивые val
    ­переменные могут освободить вас как программиста от необ­
    ходимости обдумывать порядок расстановки определений val
    ­переменных, чтобы гарантировать, что к моменту востребованности все будет определено.
    Но данное преимущество сохраняется до тех пор, пока инициализация ле­
    нивых val
    ­переменных не производит никаких побочных эффектов, а также

    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) = {}
    Эти новые определения класса компилируются без ошибок. При попытке за­
    пустить с новыми определениями класс контрпримера про коров, которые едят рыбу (cows­that­eat­fish), будут получены следующие ошибки компиляции:
    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 называется типом, зависящим от пути
    (path­dependent 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


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