layout | title | scala3 | partof | overview-name | type | description | language | num | previous-page | next-page | versionSpecific |
---|---|---|---|---|---|---|---|---|---|---|---|
multipage-overview |
Непрозрачные типы |
true |
scala3-book |
Scala 3 — Book |
section |
В этом разделе представлены и демонстрируются непрозрачные типы в Scala 3. |
ru |
55 |
types-variance |
types-structural |
true |
Непрозрачные псевдонимы типов (opaque type aliases) обеспечивают абстракцию типов без каких-либо накладных расходов.
В Scala 2 аналогичный результат можно получить с помощью [классов значений][value-classes].
Предположим, что необходимо определить модуль, предлагающий арифметические операции над числами, которые представлены их логарифмами. Это может быть полезно для повышения точности, когда числовые значения очень большие или близкие к нулю.
Поскольку важно отличать "обычные" Double
от чисел, хранящихся в виде их логарифмов, введем класс Logarithm
:
class Logarithm(protected val underlying: Double):
def toDouble: Double = math.exp(underlying)
def + (that: Logarithm): Logarithm =
// здесь используется метод apply сопутствующего объекта
Logarithm(this.toDouble + that.toDouble)
def * (that: Logarithm): Logarithm =
new Logarithm(this.underlying + that.underlying)
object Logarithm:
def apply(d: Double): Logarithm = new Logarithm(math.log(d))
Метод apply
сопутствующего объекта позволяет создавать значения типа Logarithm
,
которые можно использовать следующим образом:
val l2 = Logarithm(2.0)
val l3 = Logarithm(3.0)
println((l2 * l3).toDouble) // выводит 6.0
println((l2 + l3).toDouble) // выводит 4.999...
В то время как класс Logarithm
предлагает хорошую абстракцию для значений Double
,
которые хранятся в этой конкретной логарифмической форме,
это накладывает серьезные накладные расходы на производительность:
для каждой отдельной математической операции нужно извлекать значение underlying
,
а затем снова обернуть его в новый экземпляр Logarithm
.
Рассмотрим другой подход к реализации той же библиотеки.
На этот раз вместо того, чтобы определять Logarithm
как класс, определяем его с помощью псевдонима типа.
Во-первых, зададим абстрактный интерфейс модуля:
trait Logarithms:
type Logarithm
// операции на Logarithm
def add(x: Logarithm, y: Logarithm): Logarithm
def mul(x: Logarithm, y: Logarithm): Logarithm
// функции конвертации между Double и Logarithm
def make(d: Double): Logarithm
def extract(x: Logarithm): Double
// методы расширения, для вызова `add` и `mul` в качестве "методов" на Logarithm
extension (x: Logarithm)
def toDouble: Double = extract(x)
def + (y: Logarithm): Logarithm = add(x, y)
def * (y: Logarithm): Logarithm = mul(x, y)
Теперь давайте реализуем этот абстрактный интерфейс, задав тип Logarithm
равным Double
:
object LogarithmsImpl extends Logarithms:
type Logarithm = Double
// операции на Logarithm
def add(x: Logarithm, y: Logarithm): Logarithm = make(x.toDouble + y.toDouble)
def mul(x: Logarithm, y: Logarithm): Logarithm = x + y
// функции конвертации между Double и Logarithm
def make(d: Double): Logarithm = math.log(d)
def extract(x: Logarithm): Double = math.exp(x)
В рамках реализации LogarithmsImpl
уравнение Logarithm = Double
позволяет реализовать различные методы.
Однако эта абстракция немного "дырява".
Мы должны убедиться, что всегда программируем только с абстрактным интерфейсом Logarithms
и никогда не используем LogarithmsImpl
напрямую.
Прямое использование LogarithmsImpl
сделало бы равенство Logarithm = Double
видимым для пользователя,
который может случайно использовать Double
там, где ожидается логарифмическое удвоение.
Например:
import LogarithmsImpl.*
val l: Logarithm = make(1.0)
val d: Double = l // проверка типов ДОЗВОЛЯЕТ равенство!
Необходимость разделения модуля на абстрактный интерфейс и реализацию может быть полезной,
но также требует больших усилий, чтобы просто скрыть детали реализации Logarithm
.
Программирование с использованием абстрактного модуля Logarithms
может быть очень утомительным
и часто требует использования дополнительных функций, таких как типы, зависящие от пути, как в следующем примере:
def someComputation(L: Logarithms)(init: L.Logarithm): L.Logarithm = ...
Абстракции типов, такие как type Logarithm
, стираются
в соответствии с их привязкой (Any
- в нашем случае).
То есть, хотя нам не нужно вручную переносить и разворачивать значение Double
,
все равно будут некоторые накладные расходы, связанные с упаковкой примитивного типа Double
.
Вместо того чтобы вручную разбивать компонент Logarithms
на абстрактную часть и на конкретную реализацию,
можно просто использовать opaque типы для достижения аналогичного эффекта:
object Logarithms:
//vvvvvv это важное различие!
opaque type Logarithm = Double
object Logarithm:
def apply(d: Double): Logarithm = math.log(d)
extension (x: Logarithm)
def toDouble: Double = math.exp(x)
def + (y: Logarithm): Logarithm = Logarithm(math.exp(x) + math.exp(y))
def * (y: Logarithm): Logarithm = x + y
Тот факт, что Logarithm
совпадает с Double
, известен только в области, где он определен,
которая в приведенном выше примере соответствует объекту Logarithms
.
Равенство Logarithm = Double
может использоваться для реализации методов (например, *
и toDouble
).
Однако вне модуля тип Logarithm
полностью инкапсулирован или «непрозрачен».
Для пользователей Logarithm
-а невозможно обнаружить, что Logarithm
на самом деле реализован как Double
:
import Logarithms.*
val log2 = Logarithm(2.0)
val log3 = Logarithm(3.0)
println((log2 * log3).toDouble) // выводит 6.0
println((log2 + log3).toDouble) // выводит 4.999...
val d: Double = log2 // ERROR: Found Logarithm required Double
Несмотря на то, что мы абстрагировались от Logarithm
, абстракция предоставляется бесплатно:
поскольку существует только одна реализация, во время выполнения не будет накладных расходов
на упаковку для примитивных типов, таких как Double
.
Непрозрачные типы предлагают надежную абстракцию над деталями реализации, не накладывая расходов на производительность. Как показано выше, непрозрачные типы удобны в использовании и очень хорошо интегрируются с [функцией методов расширения][extension].
[extension]: {% link _overviews/scala3-book/ca-extension-methods.md %} [value-classes]: {% link _overviews/core/value-classes.md %}