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

  • Листинг 16.1.

  • Листинг 16.2.

  • Листинг 16.3.

  • Листинг 16.4.

  • Листинг 16.5.

  • Листинг 16.6.

  • Листинг 16.7.

  • Листинг 16.8.

  • 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
    страница38 из 64
    1   ...   34   35   36   37   38   39   40   41   ...   64
    351
    ращение cs.head
    , над списком cs будет проделано произвольное количество других операций.
    Что же касается изменяемого объекта, то результат вызова метода или об­
    ращения к полю может зависеть от того, какие операции были ранее выпол­
    нены в отношении объекта. Хороший пример изменяемого объекта — бан­
    ковский счет. Его упрощенная реализация показана в листинге 16.1.
    Листинг 16.1. Изменяемый класс банковского счета class BankAccount:
    private var bal: Int = 0
    def balance: Int = bal def deposit(amount: Int): Unit =
    require(amount > 0)
    bal += amount def withdraw(amount: Int): Boolean =
    if amount > bal then false else bal -= amount true
    В классе
    BankAccount определяются приватная переменная bal и три пу­
    бличных метода: balance возвращает текущий баланс, deposit добавляет к bal заданную сумму, withdraw предпринимает попытку вывести из bal заданную сумму, гарантируя при этом, что баланс не станет отрицательным.
    Возвращаемое withdraw значение, имеющее тип
    Boolean
    , показывает, были ли запрошенные средства успешно выведены.
    Даже если ничего не знать о внутренней работе класса
    BankAccount
    , все же можно сказать, что экземпляры
    BankAccounts являются изменяемыми объ­
    ектами:
    val account = new BankAccount account.deposit(100)
    account.withdraw(80) // true account.withdraw(80) // false
    Обратите внимание: в двух последних операциях вывода средств в ходе работы с программой были возвращены разные результаты. По выполнении первой операции было возвращено значение true
    , поскольку на банковском счете со­
    держался достаточный объем, позволяющий вывести средства. Вторая опера­
    ция вывода средств была такой же, как и первая, однако по ее выполнении было

    352 Глава 16 • Изменяемые объекты возвращено значение false
    , поскольку баланс счета уменьшился настолько, что уже не мог покрыть запрошенные средства. Исходя из этого, мы понимаем, что банковским счетам присуще изменяемое состояние, так как при выполнении одной и той же операции в разное время получаются разные результаты.
    Можно подумать, будто изменяемость
    BankAccount априори не вызывает сомнений, поскольку в нем содержится определение var
    ­переменной. Из­
    меняемость и var
    ­переменные обычно идут рука об руку, но ситуация не всегда бывает столь очевидной. Например, класс может быть изменяемым и без определения или наследования каких­либо var
    ­переменных, поскольку перенаправляет вызовы методов другим объектам, которые находятся в изме­
    няемом состоянии. Может сложиться и обратная ситуация: класс содержит var
    ­переменные и все же является чисто функциональным. Как образец, можно привести класс, кэширующий результаты дорогой операции в поле в целях оптимизации. Чтобы подобрать пример, предположим наличие не­
    оптимизированного класса
    Keyed с дорогой операцией computeKey
    :
    class Keyed:
    def computeKey: Int = ... // займет некоторое время
    При условии, что computeKey не читает и не записывает никаких var
    ­пере­
    менных, эффективность
    Keyed можно увеличить, добавив кэш:
    class MemoKeyed extends Keyed:
    private var keyCache: Option[Int] = None override def computeKey: Int =
    if !keyCache.isDefined then keyCache = Some(super.computeKey)
    keyCache.get
    Использование
    MemoKeyed вместо
    Keyed может ускорить работу: когда резуль­
    тат выполнения операции computeKey будет запрошен повторно, вместо еще одного запуска computeKey может быть возвращено значение, сохраненное в поле keyCache
    . Но за исключением такого ускорения поведение классов
    Keyed и
    MemoKeyed абсолютно одинаково. Следовательно, если
    Keyed является чисто функциональным классом, то таковым будет и класс
    MemoKeyed
    , даже притом что содержит переназначаемую переменную.
    16 .2 . Переназначаемые переменные и свойства
    В отношении переназначаемой переменной допускается выполнение двух основных операций: получения ее значения или присваивания ей нового.

    16 .2 . Переназначаемые переменные и свойства 353
    В таких библиотеках, как JavaBeans, эти операции часто инкапсулированы в отдельные методы считывания (getter) и записи значения (setter), которые необходимо объявлять явно.
    В Scala каждая var
    ­переменная представляет собой неприватный член ка­
    кого­либо объекта, в отношении которого в нем неявно определены методы геттер и сеттер. Но названия таких методов отличаются от предписанных соглашениями Java. Метод получения значения (геттер) var
    ­переменной x
    называется просто x
    , а метод присваивания значения (сеттер) — x_=
    Например, появляясь в классе, определение var
    ­переменной var hour = 12
    создает геттер hour и сеттер hour_=
    вдобавок к переназначаемому полю, у ко­
    торого всегда имеется внутренняя пометка "object private"
    . Она означает, что доступ к полю устанавливается только из объекта, который его содержит.
    В то же время геттер и сеттер обеспечивают исходной var
    ­переменной не­
    которую видимость. Если var
    ­переменная объявлена публичной (public), то таковыми же являются и геттер, и сеттер. Если она является защищенной
    (protected), то и они тоже, и т. д.
    Рассмотрим, к примеру, класс
    Time
    , показанный в листинге 16.2, в котором определены две публичные var
    ­переменные с именами hour и minute
    Листинг 16.2. Класс с публичными var-переменными class Time:
    var hour = 12
    var minute = 0
    Эта реализация в точности соответствует определению класса, показанного в листинге 16.3. В данном определении имена локальных полей h
    и m
    были вы­
    браны произвольно, чтобы не конфликтовали с уже используемыми именами.
    Листинг 16.3. Как публичные var-переменные расширяются в геттер и сеттер class Time:
    private var h = 12
    private var m = 0
    def hour: Int = h def hour_=(x: Int) =
    h = x def minute: Int = m def minute_=(x: Int) =
    m = x

    354 Глава 16 • Изменяемые объекты
    Интересным аспектом такого расширения var
    ­переменных в геттер и сеттер является то, что вместо определения var
    ­переменной можно также вы­
    брать вариант непосредственного определения этих методов доступа. Он позволяет как угодно интерпретировать операции доступа к переменной и присваивания ей значения. Например, вариант класса
    Time
    , показанный в листинге 16.4, содержит необходимые условия, благодаря которым пере­
    хватываются все присваивания недопустимых значений часам и минутам, хранящимся в переменных hour и minute
    Листинг 16.4. Непосредственное определение геттера и сеттера class Time:
    private var h = 12
    private var m = 0
    def hour: Int = h def hour_=(x: Int) =
    require(0 <= x && x < 24)
    h = x def minute = m def minute_=(x: Int) =
    require(0 <= x && x < 60)
    m = x
    В некоторых языках для этих похожих на переменные величин, не явля­
    ющихся простыми переменными из­за того, что их геттер и сеттер могут быть переопределены, имеются специальные синтаксические конструкции.
    Например, в C# эту роль играют свойства. По сути, принятое в Scala согла­
    шение о постоянной интерпретации переменной как имеющей пару геттер и сеттер предоставляет вам такие же возможности, что и свойства C#, но при этом не требует какого­то специального синтаксиса.
    Свойства могут иметь множество назначений. В примере, показанном в ли­
    стинге 16.4, методы присваивания значений навязывают соблюдение кон­
    кретных условий, защищая таким образом переменную от присваивания ей недопустимых значений. Кроме того, свойства позволяют регистрировать все обращения к переменной со стороны геттера и сеттера. Или же можно объединять переменные с событиями, например уведомляя с помощью ме­
    тодов­подписчиков о каждом изменении переменной.
    Вдобавок возможно, а иногда и полезно определять геттер и сеттер без свя­
    занных с ними полей. Например, в листинге 16.5 показан класс
    Thermometer
    ,

    16 .2 . Переназначаемые переменные и свойства 355
    в котором инкапсулирована переменная temperature
    , позволяющая читать и обновлять ее значение. Температурные значения могут выражаться в гра­
    дусах Цельсия или Фаренгейта. Этот класс позволяет получать и устанав­
    ливать значение температуры в любых единицах измерения.
    Листинг 16.5. Определение геттера и сеттера без связанного с ними поля import scala.compiletime.uninitialized class Thermometer:
    var celsius: Float = uninitialized def fahrenheit = celsius * 9 / 5 + 32
    def fahrenheit_=(f: Float) =
    celsius = (f - 32) * 5 / 9
    override def toString = s"${fahrenheit}F/${celsius}C"
    В первой строке тела этого класса определяется var
    ­переменная celsius
    , в которой будет храниться значение температуры в градусах Цельсия.
    Для переменной celsius изначально устанавливается значение по умол­
    чанию: в качестве инициализирующего значения для нее устанавливается знак
    =
    uninitialized
    . Точнее, инициализатором поля данному полю при­
    сваивается нулевое значение. Суть нулевого значения зависит от типа поля.
    Для числовых типов это
    0
    , для булевых — false
    , а для ссылочных — null
    Получается то же самое, что и при определении в Java некой переменной без инициализатора.
    Учтите, что в Scala просто отбросить инициализатор
    =
    uninitialized нельзя.
    Если использовать код var celsius: Float то получится объявление абстрактной, а не инициализированной перемен­
    ной
    1
    За определением переменной celsius следуют геттер по имени fahrenheit и сеттер fahrenheit_=
    , которые обращаются к той же температуре, но в гра­
    дусах Фаренгейта. В листинге нет отдельного поля, содержащего значение текущей температуры в таких градусах. Вместо этого геттер и сеттер для
    1
    Абстрактные переменные будут рассматриваться в главе 20.

    356 Глава 16 • Изменяемые объекты значений в градусах Фаренгейта выполняют автоматическое преобразование из градусов Цельсия и в них же соответственно. Пример взаимодействия с объектом
    Thermometer выглядит следующим образом:
    val t = new Thermometer t // 32.0F/0.0C
    t.celsius = 100
    t // 212.0F/100.0C
    t.fahrenheit = -40
    t // -40.0F/-40.0C
    16 .3 . Практический пример: моделирование дискретных событий
    Далее в главе на расширенном примере будут показаны интересные способы возможного сочетания изменяемых объектов с функциями, являющимися значениями первого класса. Речь идет о проектировании и реализации си­
    мулятора цифровых схем. Эта задача разбита на несколько подзадач, каждая из которых интересна сама по себе.
    Сначала мы покажем весьма лаконичный язык для цифровых схем. Его определение подчеркнет общий метод встраивания предметно­ориенти­
    рованных языков (domain­specific languages, DSL) в язык их реализации, подобный Scala. Затем представим простую, но всеобъемлющую среду для моделирования дискретных событий. Ее основной задачей будет являться отслеживание действий, выполняемых в ходе моделирования. И наконец, мы покажем, как структурировать и создавать программы дискретного модели­
    рования. Цели создания таких программ — моделирование физических объ­
    ектов объектами­симуляторами и использование среды для моделирования физического времени.
    Этот пример взят из классического учебного пособия Абельсона и Суссмана
    [Abe96]. Наша ситуация отличается тем, что языком реализации является
    Scala, а не Scheme, и тем, что различные аспекты примера структурно выде­
    лены в четыре программных уровня. Первый относится к среде моделиро­
    вания, второй — к основному пакету моделирования схем, третий касается библиотеки определяемых пользователем электронных схем, а четвертый, последний уровень предназначен для каждой моделируемой схемы как та­
    ковой. Каждый уровень выражен в виде класса, и более конкретные уровни являются наследниками более общих.

    16 .4 . Язык для цифровых схем 357
    РЕЖИМ УСКОРЕННОГО ЧТЕНИЯ
    На разбор примера моделирования дискретных событий, представленного в данной главе, потребуется некоторое время . Если вы считаете, что его лучше было бы потратить на дальнейшее изучение самого языка Scala, то можете перейти к чтению следующей главы .
    16 .4 . Язык для цифровых схем
    Начнем с краткого языка для описания цифровых схем, состоящих из про-
    водников и функциональных блоков. По проводникам проходят сигналы, преобразованием которых занимаются функциональные блоки. Сигналы представлены булевыми значениями, где true используется для сигнала высокого уровня, а false
    — для сигнала низкого уровня.
    Основные функциональные блоки (или логические элементы) показаны на рис. 16.1.
    z z
    Блок «НЕ» выполняет инверсию входного сигнала.
    z z
    Блок «И» устанавливает на своем выходе конъюнкцию сигналов на входе.
    z z
    Блок «ИЛИ» устанавливает на своем выходе дизъюнкцию сигналов на входе.
    Рис. 16.1. Основные логические элементы
    Этих логических элементов вполне достаточно для построения всех осталь­
    ных функциональных блоков. У логических элементов существуют за-
    держки, следовательно, сигнал на выходе элемента будет изменяться через некоторое время после изменения сигнала на его входе.
    Элементы цифровой схемы будут описаны с применением набора классов и функций Scala. Сначала создадим класс
    Wire для проводников. Их можно сконструировать следующим образом:
    val a = new Wire val b = new Wire val c = new Wire или то же самое, но покороче:
    val a, b, c = new Wire

    358 Глава 16 • Изменяемые объекты
    Затем понадобятся три процедуры, создающие логические элементы:
    def inverter(input: Wire, output: Wire): Unit def andGate(a1: Wire, a2: Wire, output: Wire): Unit def orGate(o1: Wire, o2: Wire, output: Wire): Unit
    Необычно то, что в силу имеющегося в Scala функционального уклона логические элементы в этих процедурах вместо возвращения в качестве результата сконструированных элементов конструируются в виде побочных эффектов. Например, вызов inverter(a,
    b)
    помещает элемент «НЕ» между проводниками a
    и b
    . Получается, что данная конструкция, основанная на побочном эффекте, позволяет упростить постепенное создание все более сложных схем. Вдобавок, притом что имена большинства методов проис­
    ходят от глаголов, имена этих методов происходят от существительных, показывающих, какой именно элемент создается. Тем самым отображается декларативная природа DSL­языка: он должен давать описание электронной схемы, а не выполняемых в ней действий.
    Из логических элементов могут создаваться более сложные функциональные блоки. Например, метод, показанный в листинге 16.6, создает полусумматор.
    Метод halfAdder получает два входных параметра, a
    и b
    , и выдает сумму s
    , определяемую как s
    =
    (a
    +
    b)
    %
    2
    , и перенос в следующий разряд c
    , определя­
    емый как c
    =
    (a
    +
    b)
    /
    2
    . Схема полусумматора показана на рис. 16.2.
    Листинг 16.6. Метод halfAdder def halfAdder(a: Wire, b: Wire, s: Wire, c: Wire) =
    val d, e = new Wire orGate(a, b, d)
    andGate(a, b, c)
    inverter(c, e)
    andGate(d, e, s)
    Рис. 16.2. Схема полусумматора

    16 .4 . Язык для цифровых схем 359
    Обратите внимание: halfAdder является параметризованным функцио­
    нальным блоком, как и три метода, составляющие логические элементы.
    Его можно использовать для составления более сложных схем. Например, в листинге 16.7 определяется полный одноразрядный сумматор (рис. 16.3), который получает два входных параметра, a
    и b
    , а также перенос из младшего разряда (carry­in) cin и выдает на выходе значение sum
    , определяемое как sum
    =
    (a
    +
    b
    +
    cin)
    %
    2
    , и перенос в старший разряд (carry­out), определяемый как cout
    =
    (a
    +
    b
    +
    cin)
    /
    2
    Листинг 16.7. Метод fullAdder def fullAdder(a: Wire, b: Wire, cin: Wire,
    sum: Wire, cout: Wire) =
    val s, c1, c2 = new Wire halfAdder(a, cin, s, c1)
    halfAdder(b, s, sum, c2)
    orGate(c1, c2, cout)
    Рис. 16.3. Схема сумматора
    Класс
    Wire и функции inverter
    , andGate и orGate представляют собой крат­
    кий язык, с помощью которого пользователи могут определять цифровые схемы. Это неплохой пример внутреннего DSL — предметно­ориентирован­
    ного языка, определенного в виде не какой­то самостоятельной реализации, а библиотеки в языке его реализации.
    Реализацию DSL­языка электронных логических схем еще предстоит раз­
    работать. Цель определения схемы средствами DSL — моделирование элек­
    тронной схемы, поэтому вполне разумно будет основой реализации DSL сделать общий API для моделирования дискретных событий. В следующих двух разделах мы представим первый API моделирования, а затем в качестве надстройки над ним покажем реализацию DSL­языка электронных логиче­
    ских схем.

    360 Глава 16 • Изменяемые объекты
    16 .5 . API моделирования
    API моделирования показан в листинге 16.8. Он состоит из класса
    Simulation в пакете org.stairwaybook.simulation
    . Наследниками этого класса являются конкретные библиотеки моделирования, дополняющие его предметно­ори­
    ентированную функциональность. В данном разделе представлены элементы класса
    Simulation
    Листинг 16.8. Класс Simulation abstract class Simulation:
    type Action = () => Unit case class WorkItem(time: Int, action: Action)
    private var curtime = 0
    def currentTime: Int = curtime private var agenda: List[WorkItem] = List()
    private def insert(ag: List[WorkItem],
    item: WorkItem): List[WorkItem] =
    if ag.isEmpty || item.time < ag.head.time then item :: ag else ag.head :: insert(ag.tail, item)
    def afterDelay(delay: Int)(block: => Unit) =
    val item = WorkItem(currentTime + delay, () => block)
    agenda = insert(agenda, item)
    private def next() =
    (agenda: @unchecked) match case item :: rest =>
    agenda = rest curtime = item.time item.action()
    def run() =
    afterDelay(0) {
    println("*** simulation started, time = " +
    currentTime + " ***")
    }
    while !agenda.isEmpty do next()

    16 .5 . API моделирования 361
    При моделировании дискретных событий действия, определенные пользова­
    телем, выполняются в указанные моменты времени. Действия, определенные конкретными подклассами моделирования, имеют один и тот же тип:
    type Action = () => Unit
    Эта инструкция определяет
    Action в качестве псевдонима типа процедуры, принимающей пустой список параметров и возвращающей тип
    Unit
    . Тип
    Action является членом типа класса
    Simulation
    . Его можно рассматривать как гораздо более легко читаемое имя для типа
    ()
    =>
    Unit
    . Члены типов будут подробно рассмотрены в разделе 20.6.
    Момент времени, в который выполняется действие, является моментом мо­
    делирования — он не имеет ничего общего с временем «настенных часов».
    Моменты времени моделирования представлены просто как целые числа.
    Текущий момент хранится в приватной переменной:
    private var curtime: Int = 0
    У переменной есть публичный метод доступа, извлекающий текущее время:
    def currentTime: Int = curtime
    Это сочетание приватной переменной с публичным методом доступа служит гарантией невозможности изменить текущее время за пределами класса
    Simulation
    . Обычно не нужно, чтобы моделируемые вами объекты мани­
    пулировали текущим временем, за исключением, возможно, случая, когда моделируется путешествие во времени. Действие, которое должно быть выполнено в указанное время, называется рабочим элементом. Рабочие элементы реализуются следующим классом:
    case class WorkItem(time: Int, action: Action)
    Класс
    WorkItem сделан case
    ­классом, чтобы иметь возможность получить сле­
    дующие синтаксические удобства: для создания экземпляров класса можно использовать фабричный метод
    WorkItem и при этом без каких­либо усилий получить средства доступа к параметрам конструктора time и action
    . Следу­
    ет также заметить, что класс
    WorkItem вложен в класс
    Simulation
    . Вложенные классы в Scala обрабатываются аналогично Java. Более подробно этот вопрос рассматривается в разделе 20.7.
    В классе
    Simulation хранится план действий (agenda) всех остальных, еще не выполненных рабочих элементов. Они отсортированы по моделируемому времени, в которое должны быть запущены:
    private var agenda: List[WorkItem] = List()

    1   ...   34   35   36   37   38   39   40   41   ...   64


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