Эта страница документа относится к Scala 3 и может охватывать новые концепции, недоступные в Scala 2. Если не указано явно, все примеры кода на этой странице предполагают, что вы используете Scala 3.
Непрозрачные псевдонимы типов (opaque type aliases) обеспечивают абстракцию типов без каких-либо накладных расходов.
В Scala 2 аналогичный результат можно получить с помощью классов значений.
Накладные расходы на абстракцию
Предположим, что необходимо определить модуль, предлагающий арифметические операции над числами, которые представлены их логарифмами. Это может быть полезно для повышения точности, когда числовые значения очень большие или близкие к нулю.
Поскольку важно отличать “обычные” 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
.
Обзор непрозрачных типов
Непрозрачные типы предлагают надежную абстракцию над деталями реализации, не накладывая расходов на производительность. Как показано выше, непрозрачные типы удобны в использовании и очень хорошо интегрируются с функцией методов расширения.
Contributors to this page:
Contents
- Введение
- Возможности Scala
- Почему Scala 3?
- Почувствуй Scala
- Пример 'Hello, World!'
- REPL
- Переменные и типы данных
- Структуры управления
- Моделирование данных
- Методы
- Функции первого класса
- Одноэлементные объекты
- Коллекции
- Контекстные абстракции
- Верхнеуровневые определения
- Обзор
- Первый взгляд на типы
- Интерполяция строк
- Структуры управления
- Моделирование предметной области
- Инструменты
- Моделирование ООП
- Моделирование ФП
- Методы
- Особенности методов
- Main методы в Scala 3
- Обзор
- Функции
- Анонимные функции
- Параметры функции
- Eta расширение
- Функции высшего порядка
- Собственный map
- Создание метода, возвращающего функцию
- Обзор
- Пакеты и импорт
- Коллекции в Scala
- Типы коллекций
- Методы в коллекциях
- Обзор
- Функциональное программирование
- Что такое функциональное программирование?
- Неизменяемые значения
- Чистые функции
- Функции — это значения
- Функциональная обработка ошибок
- Обзор
- Типы и система типов
- Определение типов
- Параметризованные типы
- Пересечение типов
- Объединение типов
- Алгебраические типы данных
- Вариантность
- Непрозрачные типы
- Структурные типы
- Зависимые типы функций
- Другие типы
- Контекстные абстракции
- Методы расширения
- Параметры контекста
- Контекстные границы
- Given импорты
- Классы типов
- Многостороннее равенство
- Неявное преобразование типов
- Обзор
- Параллелизм
- Scala утилиты
- Сборка и тестирование проектов Scala с помощью Sbt
- Рабочие листы
- Взаимодействие с Java