0Pricing
Scala for Backend Engineering & Functional Programming · Урок

Newtype-типы

Типобезопасные обёртки

«Newtype-типы» — бесплатный урок Scala for Backend Engineering & Functional Programming на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Scala for Backend Engineering & Functional Programming, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Scala for Backend Engineering & Functional Programming содержит 4 уроков всего.

Типобезопасные обёртки

Новый тип — это отдельный тип, оборачивающий одно базовое значение. Он предотвращает смешивание значений с одинаковым примитивным представлением, например UserId и ProductId, которые оба имеют тип Int.

case class UserId(value: Int)
case class ProductId(value: Int)

object Main:
  def main(args: Array[String]): Unit =
    val u = UserId(1)
    val p = ProductId(1)
    println(u.value == p.value) // values equal, types differ

Проблема чрезмерной зависимости от примитивов

Чрезмерная зависимость от примитивов — это чрезмерное использование String и Int для понятий предметной области. Из-за этого можно передать email туда, где требуется имя. Новые типы исправляют проблему, предоставляя каждому понятию собственный тип.

case class Email(value: String)
case class City(value: String)

object Main:
  def greet(c: City): String = s"Welcome to ${c.value}"

  def main(args: Array[String]): Unit =
    println(greet(City("Berlin")))

Новые типы на основе case class

Самый простой новый тип — однопольный case class. Он изначально предоставляет сравнение, удобный toString и сопоставление с образцом. Недостаток — выделение одного объекта для каждого значения.

case class OrderId(value: String)

object Main:
  def main(args: Array[String]): Unit =
    val id = OrderId("ORD-42")
    println(id)
    println(id.value)

Новые типы на основе непрозрачных типов

Чтобы избежать выделения памяти, реализуйте новый тип с помощью непрозрачного типа. Во время выполнения он ведёт себя как базовый тип, но во время компиляции является отдельным.

object Ids:
  opaque type UserId = Int
  def apply(i: Int): UserId = i
  extension (u: UserId) def value: Int = u

object Main:
  def main(args: Array[String]): Unit =
    val u = Ids(7)
    println(u.value)

Добавление поведения с помощью расширений

Добавляйте операции к новому типу с помощью методов extension, открывая только те возможности, которые имеют смысл для понятия предметной области.

object Money:
  opaque type Cents = Long
  def apply(n: Long): Cents = n
  extension (c: Cents)
    def +(o: Cents): Cents = c + o
    def toDollars: Double = c / 100.0

object Main:
  def main(args: Array[String]): Unit =
    val total = Money(250) + Money(750)
    println(total.toDollars)

Новые типы в сигнатурах функций

Новые типы делают сигнатуры самодокументируемыми и обнаруживают ошибки в порядке аргументов во время компиляции. Вы не сможете случайно поменять местами два параметра разных новых типов.

case class Width(value: Int)
case class Height(value: Int)

object Main:
  def area(w: Width, h: Height): Int = w.value * h.value

  def main(args: Array[String]): Unit =
    println(area(Width(4), Height(5)))

Новые типы и коллекции

Map с ключом-новым типом понятнее и безопаснее, чем карта с ключом типа Int. Тип не позволяет использовать ключ неправильного вида.

case class UserId(value: Int)

object Main:
  def main(args: Array[String]): Unit =
    val names = Map(UserId(1) -> "Ada", UserId(2) -> "Bob")
    println(names(UserId(2)))

Проверяемые новые типы

Объедините новый тип с умным конструктором, чтобы обёртка также обеспечивала инварианты. Например, NonEmptyString никогда не может быть пустой.

case class NonEmptyString private (value: String)
object NonEmptyString:
  def of(s: String): Option[NonEmptyString] =
    if s.nonEmpty then Some(NonEmptyString(s)) else None

object Main:
  def main(args: Array[String]): Unit =
    println(NonEmptyString.of("hi"))
    println(NonEmptyString.of(""))

Выбор представления

Выбирайте представление в зависимости от потребностей.

  • case class: проще всего, поддерживает сопоставление с образцом, выделяет объект.
  • opaque type: не требует затрат, не использует упаковку, идеально подходит для критичных по производительности участков и больших коллекций.
object Temp:
  opaque type Kelvin = Double
  def apply(d: Double): Kelvin = d
  extension (k: Kelvin) def value: Double = k

object Main:
  def main(args: Array[String]): Unit =
    val readings = List(Temp(300.0), Temp(310.5))
    println(readings.map(_.value).sum)

Предотвращение случайных преобразований

Два новых типа на основе одной и той же базы не преобразуются неявно. Для перехода между ними нужно написать явную функцию, документирующую намерение.

case class Meters(value: Double)
case class Feet(value: Double)

object Main:
  def toFeet(m: Meters): Feet = Feet(m.value * 3.281)

  def main(args: Array[String]): Unit =
    println(toFeet(Meters(2.0)))

Когда использовать новые типы

Используйте новые типы всегда, когда примитив несёт смысл предметной области.

  • Идентификаторы, единицы измерения, строки заданного формата.
  • Используйте case class для простоты, а непрозрачный тип — для производительности.
  • Добавляйте умный конструктор, если существуют инварианты.
object Domain:
  opaque type Sku = String
  def of(s: String): Option[Sku] =
    if s.startsWith("SKU-") then Some(s) else None
  extension (k: Sku) def raw: String = k

object Main:
  def main(args: Array[String]): Unit =
    println(Domain.of("SKU-9").map(_.raw))

Быстрая проверка

Проверьте, насколько хорошо Вы поняли новые типы.

Итоги

Вы изучили новые типы.

  • Новые типы оборачивают одно базовое значение в отдельный тип.
  • Они устраняют чрезмерную зависимость от примитивов и предотвращают путаницу аргументов.
  • Используйте case class для простоты, а opaque type — для отсутствия затрат.
  • Добавляйте умный конструктор для проверяемых оболочек.
  • Преобразования между новыми типами всегда должны быть явными.
object Ids:
  opaque type AccountId = Long
  def apply(n: Long): AccountId = n
  extension (a: AccountId) def value: Long = a

object Main:
  def main(args: Array[String]): Unit =
    val acc = Ids(1001L)
    println(acc.value)

Часто задаваемые вопросы

Урок «Newtype-типы» бесплатный?

Да — полный текст урока «Newtype-типы» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Scala for Backend Engineering & Functional Programming, подпишись на CoddyKit PRO. Курс Scala for Backend Engineering & Functional Programming содержит 4 уроков всего.

Чему я научусь в уроке «Newtype-типы»?

Типобезопасные обёртки Ты практикуешь Scala for Backend Engineering & Functional Programming с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Scala for Backend Engineering & Functional Programming?

Предыдущий опыт не требуется. Scala for Backend Engineering & Functional Programming на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.

Сколько времени занимает урок «Newtype-типы»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Scala for Backend Engineering & Functional Programming?

Да. Каждый урок Scala for Backend Engineering & Functional Programming включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Моделирование с помощью ADT
  2. Умные конструкторы
  3. Newtype-типы
  4. Составные предметные области
← Назад к Scala for Backend Engineering & Functional Programming