Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 735
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
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
, а также перенос из младшего разряда (carryin) cin и выдает на выходе значение sum
, определяемое как sum
=
(a
+
b
+
cin)
%
2
, и перенос в старший разряд (carryout), определяемый как 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
362 Глава 16 • Изменяемые объекты
Список agenda будет храниться в надлежащем отсортированном порядке благодаря использованию метода insert
, который обновляет этот список.
Вызов метода insert в качестве единственного способа добавить рабочий элемент к плану действий можно увидеть в методе afterDelay
:
def afterDelay(delay: Int)(block: => Unit) =
val item = WorkItem(currentTime + delay, () => block)
agenda = insert(agenda, item)
Как следует из названия, этот метод вставляет действие, задаваемое блоком, в план действий, планируя время задержки его выполнения delay после теку
щего момента моделируемого времени. Например, следующий вызов создаст новый рабочий элемент к выполнению в моделируемое время currentTime
+
delay
:
afterDelay(delay) { count += 1 }
Код, предназначенный для выполнения, содержится во втором аргументе ме
тода. Формальный параметр имеет тип
=>
Unit
, то есть это вычисление типа
Unit
, передаваемое по имени. Следует напомнить, что параметры, передава
емые по имени (byname parameters), при передаче методу не вычисляются.
Следовательно, в показанном ранее вызове значение count будет увеличено на единицу, только когда среда моделирования вызовет действие, сохранен
ное в рабочем элементе. Обратите внимание: afterDelay
— каррированная функция. Это хороший пример разъясненного в разделе 9.5 принципа, со
гласно которому карринг может использоваться для выполнения вызовов методов, больше похожих на встроенный синтаксис языка.
Созданный рабочий элемент еще нужно вставить в план действий. Это дела
ется с помощью метода insert
, в котором поддерживается предварительное условие отсортированности плана по времени:
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)
Ядро класса
Simulation определяется методом run
:
def run() =
afterDelay(0) {
println("*** simulation started, time = " +
currentTime + " ***")
}
while !agenda.isEmpty do next()
16 .5 . API моделирования 363
Этот метод периодически берет первый элемент из плана действий, удаляет его из данного плана и выполняет. Он продолжает свою работу до тех пор, пока в плане не останется элементов для выполнения. Каждый шаг выпол
няется с помощью вызова метода next
, имеющего такое определение:
private def next() =
(agenda: @unchecked) match case item :: rest =>
agenda = rest curtime = item.time item.action()
В методе next текущий план действий разбивается с помощью сопоставления с образцом на первый элемент item и весь остальной список рабочих эле
ментов rest
. Первый элемент из текущего плана удаляется, моделируемое время curtime устанавливается на время рабочего элемента, и выполняется действие рабочего элемента.
Обратите внимание: next может быть вызван, только если план действий еще не пуст. Варианта для пустого плана нет, поэтому при попытке запуска next в отношении пустого списка agenda будет выдано исключение
MatchError
В действительности же компилятор Scala обычно выдает предупреждение о том, что для списка не указан один из возможных паттернов:
27 | agenda match
| ˆˆˆˆˆˆ
| match may not be exhaustive.
|
| It would fail on pattern case: Nil
В данном случае неуказанный вариант никакой проблемы не создает, по
скольку известно, что next вызывается только в отношении непустого плана действий. Поэтому может возникнуть желание отключить предупреждение.
Как было показано в разделе 13.5, это можно сделать, добавив к выражению селектора сопоставления с образцом аннотацию
@unchecked
. Именно поэтому в коде
Simulation используется
(agenda:
@unchecked)
match
, а не agenda match
И это правильно. Объем кода для среды моделирования может показаться весьма скромным. Может возникнуть вопрос: а как эта среда вообще может поддерживать содержательное моделирование, если всего лишь выполняет список рабочих элементов? В действительности же эффективность среды моделирования определяется тем фактом, что действия, сохраненные в ра
бочих элементах, в ходе своего выполнения могут самостоятельно добавлять следующие рабочие элементы в план действий. Тем самым открывается
364 Глава 16 • Изменяемые объекты возможность получить из вычисления простых начальных действий доволь
но продолжительную симуляцию.
16 .6 . Моделирование электронной логической схемы
Следующим шагом станет использование среды моделирования в целях реализации предметноориентированного языка для логических схем, пока
занного в разделе 16.4. Следует напомнить, что DSL логических схем состоит из класса для проводников и методов, создающих логические элементы «И»,
«ИЛИ» и «НЕ». Все это содержится в классе
BasicCircuitSimulation
, кото
рый расширяет среду моделирования. Он показан в листинге 16.9.
Листинг 16.9. Класс BasicCircuitSimulation package org.stairwaybook.simulation abstract class BasicCircuitSimulation extends Simulation:
def InverterDelay: Int def AndGateDelay: Int def OrGateDelay: Int class Wire:
private var sigVal = false private var actions: List[Action] = List.empty def getSignal = sigVal def setSignal(s: Boolean) =
if s != sigVal then sigVal = s actions.foreach(_())
def addAction(a: Action) =
actions = a :: actions a()
def inverter(input: Wire, output: Wire) =
def invertAction() =
val inputSig = input.getSignal afterDelay(InverterDelay) {
output setSignal !inputSig
}
input addAction invertAction
16 .6 . Моделирование электронной логической схемы 365
def andGate(a1: Wire, a2: Wire, output: Wire) =
def andAction() =
val a1Sig = a1.getSignal val a2Sig = a2.getSignal afterDelay(AndGateDelay) {
output setSignal (a1Sig & a2Sig)
}
a1 addAction andAction a2 addAction andAction def orGate(o1: Wire, o2: Wire, output: Wire) =
def orAction() =
val o1Sig = o1.getSignal val o2Sig = o2.getSignal afterDelay(OrGateDelay) {
output setSignal (o1Sig | o2Sig)
}
o1 addAction orAction o2 addAction orAction def probe(name: String, wire: Wire) =
def probeAction() =
println(name + " " + currentTime +
" new-value = " + wire.getSignal)
wire addAction probeAction
В классе
BasicCircuitSimulation объявляются три абстрактных метода, представляющих задержки основных логических элементов:
InverterDelay
,
AndGateDelay и
OrGateDelay
. Настоящие задержки на уровне этого клас
са неизвестны, поскольку зависят от технологии моделируемых логи
ческих микросхем. Поэтому задержки в классе
BasicCircuitSimulation остаются абстрактными, и их конкретное определение делегируется под
классам
1
. Далее мы рассмотрим реализацию остальных членов класса
BasicCircuitSimulation
Класс Wire
Проводникам нужно поддерживать три основных действия:
z z
getSignal:
Boolean возвращает текущий сигнал в проводнике;
z z
setSignal(sig:
Boolean)
выставляет сигнал проводника в sig
;
1
Имена этих методов задержки начинаются с прописных букв, поскольку представ
ляют собой константы. Но это методы, и они могут быть переопределены в под
классах. Как те же вопросы решаются с помощью val
переменных, мы покажем в разделе 20.3.
366 Глава 16 • Изменяемые объекты z
z addAction(p:
Action)
прикрепляет указанную процедуру p
к действиям проводника. Замысел заключается в том, чтобы все процедуры действий, прикрепленные к какомулибо проводнику, выполнялись всякий раз, ко
гда сигнал на проводнике изменяется. Как правило, действия добавляют
ся к проводнику подключенными к нему компонентами. Прикрепленное действие выполняется в момент его добавления к проводнику, а после этого всякий раз при изменении сигнала в проводнике.
Реализация класса
Wire имеет следующий вид:
class Wire:
private var sigVal = false private var actions: List[Action] = List.empty def getSignal = sigVal def setSignal(s: Boolean) =
if s != sigVal then sigVal = s actions.foreach(_())
def addAction(a: Action) =
actions = a :: actions a()
Состояние проводника формируется двумя приватными переменными.
Переменная sigVal представляет текущий сигнал, а переменная actions
— процедуры действий, прикрепленные в данный момент к проводнику. В реа
лизациях методов представляет интерес только та часть, которая относится к методу setSignal
: когда сигнал проводника изменяется, в переменной sigVal сохраняется новое значение. Кроме того, выполняются все действия, прикрепленные к проводнику. Обратите внимание на используемую для этого сокращенную форму синтаксиса: выражение actions foreach
(_())
вызывает применение функции
_()
к каждому элементу в списке действий.
В соответствии с описанием, приведенным в разделе 8.5, функция
_()
явля
ется сокращенной формой записи для f
=>
f
()
, то есть получает функцию
(назовем ее f
) и применяет ее к пустому списку параметров.
Метод inverter
Единственный результат создания инвертора — то, что действие устанав
ливается на его входном проводнике. Данное действие вызывается при его установке, а затем всякий раз при изменении сигнала на входе. Эффект