|
| 1 | +--- |
| 2 | +layout: multipage-overview |
| 3 | +title: Непрозрачные типы |
| 4 | +scala3: true |
| 5 | +partof: scala3-book |
| 6 | +overview-name: "Scala 3 — Book" |
| 7 | +type: section |
| 8 | +description: В этом разделе представлены и демонстрируются непрозрачные типы в Scala 3. |
| 9 | +language: ru |
| 10 | +num: 55 |
| 11 | +previous-page: types-variance |
| 12 | +next-page: types-structural |
| 13 | +--- |
| 14 | + |
| 15 | +В Scala 3 _непрозрачные псевдонимы типов_ (_opaque type aliases_) обеспечивают абстракции типов без каких-либо **накладных расходов**. |
| 16 | + |
| 17 | +## Накладные расходы на абстракцию |
| 18 | + |
| 19 | +Предположим, что необходимо определить модуль, |
| 20 | +предлагающий арифметические операции над числами, которые представлены их логарифмами. |
| 21 | +Это может быть полезно для повышения точности, когда числовые значения очень большие или близкие к нулю. |
| 22 | + |
| 23 | +Поскольку важно отличать "обычные" `Double` от чисел, хранящихся в виде их логарифмов, введем класс `Logarithm`: |
| 24 | + |
| 25 | +```scala |
| 26 | +class Logarithm(protected val underlying: Double): |
| 27 | + def toDouble: Double = math.exp(underlying) |
| 28 | + def + (that: Logarithm): Logarithm = |
| 29 | + // здесь используется метод apply сопутствующего объекта |
| 30 | + Logarithm(this.toDouble + that.toDouble) |
| 31 | + def * (that: Logarithm): Logarithm = |
| 32 | + new Logarithm(this.underlying + that.underlying) |
| 33 | + |
| 34 | +object Logarithm: |
| 35 | + def apply(d: Double): Logarithm = new Logarithm(math.log(d)) |
| 36 | +``` |
| 37 | + |
| 38 | +Метод `apply` сопутствующего объекта позволяет создавать значения типа `Logarithm`, |
| 39 | +которые можно использовать следующим образом: |
| 40 | + |
| 41 | +```scala |
| 42 | +val l2 = Logarithm(2.0) |
| 43 | +val l3 = Logarithm(3.0) |
| 44 | +println((l2 * l3).toDouble) // выводит 6.0 |
| 45 | +println((l2 + l3).toDouble) // выводит 4.999... |
| 46 | +``` |
| 47 | + |
| 48 | +В то время как класс `Logarithm` предлагает хорошую абстракцию для значений `Double`, |
| 49 | +которые хранятся в этой конкретной логарифмической форме, |
| 50 | +это накладывает серьезные накладные расходы на производительность: |
| 51 | +для каждой отдельной математической операции нужно извлекать значение `underlying`, |
| 52 | +а затем снова обернуть его в новый экземпляр `Logarithm`. |
| 53 | + |
| 54 | +## Модульные абстракции |
| 55 | + |
| 56 | +Рассмотрим другой подход к реализации той же библиотеки. |
| 57 | +На этот раз вместо того, чтобы определять `Logarithm` как класс, определяем его с помощью _псевдонима типа_. |
| 58 | +Во-первых, зададим абстрактный интерфейс модуля: |
| 59 | + |
| 60 | +```scala |
| 61 | +trait Logarithms: |
| 62 | + |
| 63 | + type Logarithm |
| 64 | + |
| 65 | + // операции на Logarithm |
| 66 | + def add(x: Logarithm, y: Logarithm): Logarithm |
| 67 | + def mul(x: Logarithm, y: Logarithm): Logarithm |
| 68 | + |
| 69 | + // функции конвертации между Double и Logarithm |
| 70 | + def make(d: Double): Logarithm |
| 71 | + def extract(x: Logarithm): Double |
| 72 | + |
| 73 | + // методы расширения, для вызова `add` и `mul` в качестве "методов" на Logarithm |
| 74 | + extension (x: Logarithm) |
| 75 | + def toDouble: Double = extract(x) |
| 76 | + def + (y: Logarithm): Logarithm = add(x, y) |
| 77 | + def * (y: Logarithm): Logarithm = mul(x, y) |
| 78 | +``` |
| 79 | + |
| 80 | +Теперь давайте реализуем этот абстрактный интерфейс, задав тип `Logarithm` равным `Double`: |
| 81 | + |
| 82 | +```scala |
| 83 | +object LogarithmsImpl extends Logarithms: |
| 84 | + |
| 85 | + type Logarithm = Double |
| 86 | + |
| 87 | + // операции на Logarithm |
| 88 | + def add(x: Logarithm, y: Logarithm): Logarithm = make(x.toDouble + y.toDouble) |
| 89 | + def mul(x: Logarithm, y: Logarithm): Logarithm = x + y |
| 90 | + |
| 91 | + // функции конвертации между Double и Logarithm |
| 92 | + def make(d: Double): Logarithm = math.log(d) |
| 93 | + def extract(x: Logarithm): Double = math.exp(x) |
| 94 | +``` |
| 95 | + |
| 96 | +В рамках реализации `LogarithmsImpl` уравнение `Logarithm = Double` позволяет реализовать различные методы. |
| 97 | + |
| 98 | +#### Дырявые абстракции |
| 99 | + |
| 100 | +Однако эта абстракция немного "дырява". |
| 101 | +Мы должны убедиться, что всегда программируем _только_ с абстрактным интерфейсом `Logarithms` |
| 102 | +и никогда не используем `LogarithmsImpl` напрямую. |
| 103 | +Прямое использование `LogarithmsImpl` сделало бы равенство `Logarithm = Double` видимым для пользователя, |
| 104 | +который может случайно использовать `Double` там, где ожидается логарифмическое удвоение. |
| 105 | +Например: |
| 106 | + |
| 107 | +```scala |
| 108 | +import LogarithmsImpl.* |
| 109 | +val l: Logarithm = make(1.0) |
| 110 | +val d: Double = l // проверка типов ДОЗВОЛЯЕТ равенство! |
| 111 | +``` |
| 112 | + |
| 113 | +Необходимость разделения модуля на абстрактный интерфейс и реализацию может быть полезной, |
| 114 | +но также требует больших усилий, чтобы просто скрыть детали реализации `Logarithm`. |
| 115 | +Программирование с использованием абстрактного модуля `Logarithms` может быть очень утомительным |
| 116 | +и часто требует использования дополнительных функций, таких как типы, зависящие от пути, как в следующем примере: |
| 117 | + |
| 118 | +```scala |
| 119 | +def someComputation(L: Logarithms)(init: L.Logarithm): L.Logarithm = ... |
| 120 | +``` |
| 121 | + |
| 122 | +#### Накладные расходы упаковки/распаковки |
| 123 | + |
| 124 | +Абстракции типов, такие как `type Logarithm`, [стираются](https://www.scala-lang.org/files/archive/spec/2.13/03-types.html#type-erasure) |
| 125 | +в соответствии с их привязкой (`Any` - в нашем случае). |
| 126 | +То есть, хотя нам не нужно вручную переносить и разворачивать значение `Double`, |
| 127 | +все равно будут некоторые накладные расходы, связанные с упаковкой примитивного типа `Double`. |
| 128 | + |
| 129 | +## Непрозрачные типы |
| 130 | + |
| 131 | +Вместо того чтобы вручную разбивать компонент `Logarithms` на абстрактную часть и на конкретную реализацию, |
| 132 | +можно просто использовать opaque типы для достижения аналогичного эффекта: |
| 133 | + |
| 134 | +```scala |
| 135 | +object Logarithms: |
| 136 | +//vvvvvv это важное различие! |
| 137 | + opaque type Logarithm = Double |
| 138 | + |
| 139 | + object Logarithm: |
| 140 | + def apply(d: Double): Logarithm = math.log(d) |
| 141 | + |
| 142 | + extension (x: Logarithm) |
| 143 | + def toDouble: Double = math.exp(x) |
| 144 | + def + (y: Logarithm): Logarithm = Logarithm(math.exp(x) + math.exp(y)) |
| 145 | + def * (y: Logarithm): Logarithm = x + y |
| 146 | +``` |
| 147 | + |
| 148 | +Тот факт, что `Logarithm` совпадает с `Double`, известен только в области, где он определен, |
| 149 | +которая в приведенном выше примере соответствует объекту `Logarithms`. |
| 150 | +Равенство `Logarithm = Double` может использоваться для реализации методов (например, `*` и `toDouble`). |
| 151 | + |
| 152 | +Однако вне модуля тип `Logarithm` полностью инкапсулирован или «непрозрачен». |
| 153 | +Для пользователей `Logarithm`-а невозможно обнаружить, что `Logarithm` на самом деле реализован как `Double`: |
| 154 | + |
| 155 | +```scala |
| 156 | +import Logarithms.* |
| 157 | +val log2 = Logarithm(2.0) |
| 158 | +val log3 = Logarithm(3.0) |
| 159 | +println((log2 * log3).toDouble) // выводит 6.0 |
| 160 | +println((log2 + log3).toDouble) // выводит 4.999... |
| 161 | + |
| 162 | +val d: Double = log2 // ERROR: Found Logarithm required Double |
| 163 | +``` |
| 164 | + |
| 165 | +Несмотря на то, что мы абстрагировались от `Logarithm`, абстракция предоставляется бесплатно: |
| 166 | +поскольку существует только одна реализация, во время выполнения не будет накладных расходов |
| 167 | +на упаковку для примитивных типов, таких как `Double`. |
| 168 | + |
| 169 | +### Обзор непрозрачных типов |
| 170 | + |
| 171 | +Непрозрачные типы предлагают надежную абстракцию над деталями реализации, не накладывая расходов на производительность. |
| 172 | +Как показано выше, непрозрачные типы удобны в использовании и очень хорошо интегрируются с [функцией методов расширения][extension]. |
| 173 | + |
| 174 | +[extension]: {% link _overviews/scala3-book/ca-extension-methods.md %} |
0 commit comments