Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 742
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
16 .6 . Моделирование электронной логической схемы 367
от действия заключается в установке выходного значения (с помощью setSignal
) на отрицание его входного значения. Поскольку у логического элемента «НЕ» имеется задержка, это изменение должно наступить только по прошествии определенного количества единиц моделируемого времени, хранящегося в переменной
InverterDelay
, после изменения входного значе
ния и выполнения действия. Эти обстоятельства подсказывают следующий вариант реализации:
def inverter(input: Wire, output: Wire) =
def invertAction() =
val inputSig = input.getSignal afterDelay(InverterDelay) {
output setSignal !inputSig
}
input addAction invertAction
Эффект метода inverter заключается в добавлении действия invertAction к input
. При вызове данного действия берется входной сигнал и устанавлива
ется еще одно действие, инвертирующее выходной сигнал в плане действий моделирования. Это другое действие должно быть выполнено по прошествии того количества единиц времени моделирования, которое хранится в пере
менной
InverterDelay
. Обратите внимание на то, как для создания нового рабочего элемента, предназначенного для выполнения в будущем, в методе используется метод afterDelay
Методы andGate и orGate
Реализация моделирования логического элемента «И» аналогична реали
зации моделирования элемента «НЕ». Цель — выставить на выходе конъ
юнкцию его входных сигналов. Это должно произойти по прошествии того количества единиц времени моделирования, которое хранится в переменной
AndGateDelay
, после изменения любого из его двух входных сигналов. Стало быть, подойдет следующая реализация:
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
368 Глава 16 • Изменяемые объекты
Эффект от вызова метода andGate заключается в добавлении действия andAction к обоим входным проводникам, a1
и a2
. При вызове данного действия берутся оба входных сигнала и устанавливается еще одно дей
ствие, которое выдает выходной сигнал в виде конъюнкции обоих входных сигналов. Это другое действие должно быть выполнено по прошествии того количества единиц времени моделирования, которое хранится в пере
менной
AndGateDelay
. Учтите, что при смене любого сигнала на входных проводниках выход должен вычисляться заново. Именно поэтому одно и то же действие andAction устанавливается на каждом из двух входных проводников, a1
и a2
. Метод orGate реализуется аналогичным образом, за исключением того, что моделирует логическую операцию «ИЛИ», а не «И».
Вывод симуляции
Для запуска симулятора нужен способ проверки изменения сигналов на проводниках. Чтобы выполнить эту задачу, можно смоделировать действие проверки (пробы) проводника:
def probe(name: String, wire: Wire) =
def probeAction() =
println(name + " " + currentTime +
" new-value = " + wire.getSignal)
wire addAction probeAction
Эффект от процедуры probe заключается в установке на заданный проводник действия probeAction
. Как обычно, установленное действие выполняется всякий раз при изменении сигнала на проводнике. В данном случае он про
сто выводит на стандартное устройство название проводника (которое пере
дается probe в качестве первого параметра), а также текущее моделируемое время и новое значение проводника.
Запуск симулятора
После всех этих приготовлений настало время посмотреть на симулятор в действии. Для определения конкретной симуляции нужно выполнить наследование из класса среды моделирования. Чтобы увидеть коечто ин
тересное, будет создан абстрактный класс моделирования, расширяющий
BasicCircuitSimulation и содержащий определения методов для полу
сумматора и сумматора в том виде, в котором они были представлены
16 .6 . Моделирование электронной логической схемы 369
в листингах 16.6 и 16.7 соответственно. Этот класс, который будет назван
CircuitSimulation
, показан в листинге 16.10.
Листинг 16.10. Класс CircuitSimulation package org.stairwaybook.simulation abstract class CircuitSimulation extends BasicCircuitSimulation:
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)
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)
Конкретная модель логической схемы будет объектомнаследником класса
CircuitSimulation
. Этому объекту попрежнему необходимо зафиксировать задержки на логических элементах в соответствии с технологией реализа
ции моделируемой логической микросхемы. И наконец, понадобится также определить конкретную моделируемую схему.
Эти шаги можно проделать в интерактивном режиме в интерпретаторе Scala:
scala> import org.stairwaybook.simulation.*
Сначала займемся задержками логических элементов. Определим объект
(назовем его
MySimulation
), предоставляющий несколько чисел:
scala> object MySimulation extends CircuitSimulation:
def InverterDelay = 1
def AndGateDelay = 3
def OrGateDelay = 5
// Определяем объект MySimulation
Поскольку предполагается периодически получать доступ к элементам объ
екта
MySimulation
, импортирование этого объекта укоротит последующий код:
scala> import MySimulation.*
370 Глава 16 • Изменяемые объекты
Далее займемся схемой. Определим четыре проводника и поместим пробы на два из них:
scala> val input1, input2, sum, carry = new Wire val input1: MySimulation.Wire = ...
val input2: MySimulation.Wire = ...
val sum: MySimulation.Wire = ...
val carry: MySimulation.Wire = ...
scala> probe("sum", sum)
sum 0 new-value = false scala> probe("carry", carry)
carry 0 new-value = false
Обратите внимание: пробы немедленно выводят выходные данные. Дело в том, что каждое действие, установленное на проводнике, первый раз вы
полняется при его установке.
Теперь определим подключение к проводникам полусумматора:
scala> halfAdder(input1, input2, sum, carry)
И наконец, установим один за другим сигналы на двух входящих проводни
ках на true и запустим моделирование:
scala> input1 setSignal true scala> run()
*** simulation started, time = 0 ***
sum 8 new-value = true scala> input2 setSignal true scala> run()
*** simulation started, time = 8 ***
carry 11 new-value = true sum 15 new-value = false
Резюме
В данной главе мы собрали воедино две на первый взгляд несопоставимые технологии: изменяемое состояние и функции высшего порядка. Изменяемое состояние было использовано для моделирования физических объектов, со
стояние которых со временем изменяется. Функции высшего порядка были применены в среде моделирования в целях выполнения действий в указан
Резюме 371
ные моменты моделируемого времени. Они также были использованы в мо
делировании логических схем в качестве триггеров, связывающих действия с изменениями состояния. Попутно мы показали простой способ определить предметноориентированный язык в виде библиотеки. Вероятно, для одной главы этого вполне достаточно.
Если вас привлекла эта тема, то можете попробовать создать дополнительные примеры моделирования. Можно объединить полусумматоры и сумматоры для формирования более крупных схем или на основе ранее определенных логических элементов разработать новые схемы и смоделировать их. В гла
ве 19 мы рассмотрим имеющуюся в Scala параметризацию типов и покажем еще один пример, который сочетает в себе функциональный и императивный подходы, дающие весьма неплохое решение.
17
Иерархия Scala
В этой главе мы рассмотрим иерархию классов Scala в целом. В Scala каждый класс наследуется от общего суперкласса по имени
Any
. Поскольку каждый класс является подклассом
Any
, то методы, определенные в классе
Any
, универ
сальны: их можно вызвать в отношении любого объекта. В самом низу иерар
хии в Scala также определяются довольно интересные классы
Null и
Nothing
, которые, по сути, выступают в роли общих подклассов. Например, в то время, как
Any
— суперкласс для всех классов,
Nothing
— подкласс для любого класса.
В данной главе мы проведем экскурсию по имеющейся в Scala иерархии классов.
17 .1 . Иерархия классов Scala
На рис. 17.1 в общих чертах показана иерархия классов Scala. На вершине иерархии находится класс
Any
; в нем определяются методы, в число которых входят:
final def ==(that: Any): Boolean final def !=(that: Any): Boolean def equals(that: Any): Boolean def ##: Int def hashCode: Int def toString: String
Все классы — наследники класса
Any
, поэтому каждый объект в программе на Scala можно подвергнуть сравнению с помощью
==
,
!=
или equals
, хеши
рованию с использованием
##
или hashCode и форматированию, прибегнув к toString
. Методы определения равенства
==
и неравенства
!=
объявлены в классе
Any как final
, следовательно, переопределить их в подклассах не
возможно.
17 .1 . Иерархия классов Scala 373
Рис.
17.1.
Иерархия классов S
cala
374 Глава 17 • Иерархия Scala
Метод
==
— по сути то же самое, что и equals
, а
!=
всегда является отрица
нием метода equals
1
. Таким образом, отдельные классы могут перекроить смысл значения метода
==
или
!=
, переопределив equals
Множественное равенство
В Scala 3 вводится понятие «множественное равенство», вызывающее ошибку компилятора при использовании методов
==
и
=
, которые от
ражают вероятные ошибки, например, при сравнении
String и
Int на равенство. Этот механизм будет описан в главе 23.
У корневого класса
Any имеется два подкласса:
AnyVal и
AnyRef
. Класс
AnyVal является родительским для классов значений в Scala. Наряду с возможно
стью определять собственные классы значений (см. раздел 17.4) Scala имеет девять встроенных:
Byte
,
Short
,
Char
,
Int
,
Long
,
Float
,
Double
,
Boolean и
Unit
Первые восемь соответствуют примитивным типам Java, и их значения во время выполнения программы представляются в виде примитивных значе
ний Java. Все экземпляры этих классов написаны в Scala в виде литералов.
Например,
42
,
'x'
, false
— экземпляры классов
Int
,
Char и
Boolean соот
ветственно. Создать их, используя ключевое слово new
, невозможно. Этому препятствует особый прием, в котором все классы значений определены одновременно и как абстрактные, и как финальные.
Поэтому, если воспользоваться следующим кодом:
scala> new Int то будет получен такой результат:
1 |new Int
| ˆˆˆ
| Int is abstract; it cannot be instantiated
1
Единственный случай, когда использование
==
не приводит к непосредственному вызову equals
, относится к упакованным числовым классам Java, таким как
Integer или
Long
. В Java new
Integer(1)
не эквивалентен new
Long(1)
даже в случае примене
ния примитивных значений
1
==
1L
. Поскольку Scala — более регулярный язык, чем
Java, появилась необходимость скорректировать это несоответствие, задействовав для этих классов особую версию метода
==
. Точно так же метод
##
обеспечивает
Scalaверсию хеширования и похож на Javaметод hashCode
, за исключением того, что для упакованных числовых типов он всегда работает с методом
==
. Например, для new
Integer(1)
и new
Long(1)
метод
##
вычисляет один и тот же хеш, тогда как
Javaметоды hashCode вычисляют разный хешкод.
17 .1 . Иерархия классов Scala 375
Класс значений
Unit примерно соответствует имеющемуся в Java типу void
— он используется в качестве результирующего типа выполнения ме
тода, который не возвращает содержательного результата. Как упоминалось в разделе 7.2, у
Unit имеется единственное значение экземпляра, оно запи
сывается как
()
Как объяснялось в главе 5, в классах значений в качестве методов поддер
живаются обычные арифметические и логические (булевы) операторы.
Например, у класса
Int имеются методы
+
и
*
, а у класса
Boolean
— мето
ды
||
и
&&
. Классы значений также наследуют все методы из класса
Any
Например:
42.toString // 42 42.hashCode // 42 42.equals(42) // true
Следует отметить, что пространство классов значений плоское: все классы значений являются подтипами scala.AnyVal
, но не являются подклассами друг друга. Вместо этого между различными типами классов значений существует неявное преобразование типов. Например, экземпляр класса scala.Int
, когда это требуется, автоматически расширяется (путем неявного преобразования) в экземпляр класса scala.Long
Как упоминалось в разделе 5.10, неявное преобразование используется также для добавления большей функциональности к типам значений. Например, тип
Int поддерживает все показанные далее операции:
42.max(43) // 43 42.min(43) // 42 1 until 5 // Range 1 until 5 1 to 5 // Range 1 to 5 3.abs // 3
-3.abs // 3
Работает это следующим образом: все методы min
, max
, until
, to и abs опреде
лены в классе scala.runtime.RichInt
, а между классами
Int и
RichInt суще
ствует неявное преобразование. Оно применяется при вызове в отношении
Int
объекта метода, который определен не в классе
Int
, а в
RichInt
. По ана
логии с этим «классыусилители» и неявные преобразования существуют и для других классов значений
1 1
Этот вариант использования неявных преобразований со временем будет заменен методами расширения, описанными в главе 22.
376 Глава 17 • Иерархия Scala
Другим подклассом корневого класса
Any является
AnyRef
— база всех ссылоч-
ных классов в Scala. Как упоминалось ранее, на платформе Java
AnyRef факти
чески является псевдонимом класса java.lang.Object
, а значит, все классы, написанные на Java и Scala, — наследники
AnyRef
1
. Поэтому java.lang.Object считается способом реализации
AnyRef на платформе Java. Таким образом, хоть
Object и
AnyRef и можно взаимозаменяемо использовать в программах
Scala на платформе Java, рекомендуемым стилем будет повсеместное при
менение
AnyRef
17 .2 . Как реализованы примитивы
Как все это реализовано? Фактически в Scala целочисленные значения хра
нятся так же, как и в Java, — в виде 32разрядных слов. Это необходимо для эффективной работы виртуальной машины Java (JVM) и обеспечения воз
можности совместной работы с библиотеками Java. Такие стандартные опе
рации, как сложение или умножение, реализуются в качестве примитивных операций. Однако Scala использует «резервный» класс java.lang.In teger везде, где целое число должно выглядеть как (Java) объект. Так происходит, например, при вызове метода toString для целого числа или присваива
нии этого числа переменной типа
Any
. При необходимости целочисленные значения типа
Int явно преобразуются в упакованные целые числа типа java.lang.Integer
Это во многом походит на автоупаковку (autoboxing) в Java, два процесса действительно очень похожи. Но всетаки есть одно коренное различие: упаковка в Scala гораздо менее заметна, чем в Java. Попробуйте выполнить в Java следующий код:
// Это код на языке Java boolean isEqual(int x, int y) {
return x == y;
}
System.out.println(isEqual(421, 421));
В результате, конечно же, будет получено значение true
. А теперь измените типы аргументов isEqual на java.lang.Integer
(или с аналогичным резуль
татом на
Object
):
1
Одна из причин существования псевдонима AnyRef, заменяющего использование имени java.lang.Object
, заключается в том, что Scala изначально разрабатывался для работы как на платформе Java, так и на платформе .NET. На платформе .NET
AnyRef был псевдонимом для
System.Object
17 .2 . Как реализованы примитивы 377
// Это код на языке Java boolean isEqual(Integer x, Integer y) {
return x == y;
}
System.out.println(isEqual(421, 421));
В итоге получите результат false
! Оказывается, число
421
было упаковано дважды, поэтому аргументами для x
и y
стали два разных объекта. При
менение
==
в отношении ссылочных типов означает равенство ссылок, а
Integer
— ссылочный тип, вследствие чего в результате получается false
Это один из аспектов, свидетельствующих о том, что Java не является чистым объектноориентированным языком. Существует четко видимая разница между примитивными и ссылочными типами.
Теперь попробуйте провести тот же самый эксперимент на Scala:
def isEqual(x: Int, y: Int) = x == y isEqual(421, 421) // true def isEqual(x: Any, y: Any) = x == y isEqual(421, 421) // true
Операция
==
в Scala разработана так, чтобы быть понятной относительно представления типа. Для типов значений (числовых или логических) это вполне естественное равенство. Для ссылочных типов, отличающихся от упа
кованных числовых типов Java,
==
рассматривается в качестве псевдонима метода equals
, унаследованного от класса
Object
. Данный метод изначально определен в целях выявления равенства ссылок, но во многих подклассах переопределяется для реализации их естественных представлений о равен
стве. Это также означает, что в Scala вы никогда не попадете в хорошо из
вестную в Java ловушку, касающуюся сравнения строк. В Scala оно работает вполне корректно:
val x = "abcd".substring(2) // cd val y = "abcd".substring(2) // cd x == y // true
В Java результатом сравнения x
с y
будет false
. В этом случае программист должен был воспользоваться методом equals
, но данный нюанс нетрудно упустить из виду.
Может сложиться и такая ситуация, при которой вместо равенства, опре
деляемого пользователем, нужно проверить равенство ссылок. Так, в неко
торых ситуациях, когда эффективность важнее всего, вы можете использо
вать хеш конс (hash cons) некоторых классов и сопоставить их экземпляры
378 Глава 17 • Иерархия Scala с помощью равенства ссылок
1
. Для таких случаев в классе
AnyRef определен дополнительный метод eq
, который не может быть переопределен и реализо
ван как проверка равенства ссылок (то есть для ссылочных типов ведет себя подобно
==
в Java). Существует также отрицание eq
, которое называется ne
, например:
val x = new String("abc") // abc val y = new String("abc") // abc x == y // true x eq y // false x ne y // true
Более подробно равенство в Scala рассматривается в главе 8.
17 .3 . Низшие типы
Внизу иерархии на рис. 17.1 показаны два класса: scala.Null и scala.No- thing
. Это особые типы, единообразно сглаживающие острые углы объектно
ориентированной системы типов в Scala.
Класс
Null
— тип нулевой ссылки null
: он представляет собой подкласс каждого ссылочного класса (то есть каждого класса, который сам является наследником класса
AnyRef)
2
Null несовместим с типами значений. Нельзя, к примеру, присвоить значение null целочисленной переменной:
scala> val i: Int = null
1 |val i: Int = null
| ˆˆˆˆ
| Found: Null
| Required: Int
Тип
Nothing находится в самом низу иерархии классов Scala: он представляет собой подтип любого другого типа, значений которого вообще не существу
1
Вы можете выполнить хеш конс экземпляров класса путем кэширования всех соз
данных экземпляров в слабую коллекцию. Затем, как только потребуется новый экземпляр класса, сначала проверяется кэш. Если в нем уже есть элемент, равный тому, который вы намереваетесь создать, то можно повторно воспользоваться су
ществующим экземпляром. В результате такой систематизации любые два экзем
пляра, равенство которых определяется с помощью метода equals()
, также равны на основе равенства ссылок.
2
В Scala 3 есть опция
-Yexplicit-nulls
, которая позволяет использовать экспери
ментальную альтернативную обработку значения null
, направленную на отслежи
вание переменных, которые могут и не могут быть нулевыми.