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

Типы объединения и пересечения

Новые возможности типов

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

Новые возможности типов

Scala 3 добавляет в систему типов объединяющие типы (A | B) и пересекающиеся типы (A & B). Они позволяют точнее описывать значения без иерархий наследования.

object Main:
  def main(args: Array[String]): Unit =
    println("Union: A | B, Intersection: A & B")

Основы объединяющих типов

Объединяющий тип A | B представляет значение, являющееся либо A, либо B. С точки зрения теории множеств это операция OR над типами.

object Main:
  def show(x: Int | String): String = x match
    case i: Int    => s"int $i"
    case s: String => s"str $s"

  def main(args: Array[String]): Unit =
    println(show(42))
    println(show("hi"))

Объединениям не нужен общий супертип

В отличие от Either в Scala 2, объединяющие типы не требуют обёртки. Любые два типа можно объединить, даже если у них нет общего родительского класса.

object Main:
  def parse(input: Int | Boolean): String =
    input match
      case n: Int     => s"number $n"
      case b: Boolean => s"flag $b"

  def main(args: Array[String]): Unit =
    println(parse(7))
    println(parse(true))

Объединения для моделирования ошибок

Объединяющие типы удобны для лёгкого представления результатов с ошибками без типа-обёртки. В сочетании с перечислениями или одиночными объектами они выглядят лаконично.

case object NotFound

object Main:
  def lookup(id: Int): String | NotFound.type =
    if id == 1 then "Alice" else NotFound

  def main(args: Array[String]): Unit =
    println(lookup(1))
    println(lookup(2))

Основы пересекающихся типов

Пересекающийся тип A & B представляет значение, которое является одновременно и A, и B. С точки зрения теории множеств это операция AND, часто используемая для объединения трейтов.

trait Named:
  def name: String
trait Aged:
  def age: Int

object Main:
  def describe(p: Named & Aged): String =
    s"${p.name} is ${p.age}"

  def main(args: Array[String]): Unit =
    val person = new Named with Aged:
      def name = "Bob"
      def age = 40
    println(describe(person))

Пересечение коммутативно

Порядок не имеет значения для пересекающихся типов: A & B — это тот же тип, что и B & A. Это отличается от составных типов Scala 2 A with B, для которых порядок в некоторых разрешениях членов имел значение.

trait Readable:
  def read: String
trait Writable:
  def write(s: String): Unit

object Main:
  def use(r: Readable & Writable): Unit =
    r.write("x")
    println(r.read)

  def main(args: Array[String]): Unit =
    val rw = new Readable with Writable:
      def read = "data"
      def write(s: String) = println(s"wrote $s")
    use(rw)

Объединение нескольких трейтов

Можно пересекать более двух типов. Результат требует наличия всех перечисленных возможностей.

trait A: def a: Int
trait B: def b: Int
trait C: def c: Int

object Main:
  def sum(x: A & B & C): Int = x.a + x.b + x.c

  def main(args: Array[String]): Unit =
    val v = new A with B with C:
      def a = 1
      def b = 2
      def c = 3
    println(sum(v))

Псевдонимы типов для объединений

Длинным объединяющим типам можно присвоить имя с помощью псевдонима type, чтобы сигнатуры оставались читаемыми.

type Json = Int | String | Boolean

object Main:
  def render(j: Json): String = j match
    case i: Int     => i.toString
    case s: String  => s"\"$s\""
    case b: Boolean => b.toString

  def main(args: Array[String]): Unit =
    println(render("hi"))
    println(render(true))

Полное сопоставление объединений

Компилятор знает элементы объединения, поэтому может проверить полноту сопоставления. Обработка каждого варианта делает сопоставление исчерпывающим.

type Shape = "circle" | "square"

object Main:
  def sides(s: Shape): Int = s match
    case "circle" => 0
    case "square" => 4

  def main(args: Array[String]): Unit =
    println(sides("square"))

Расширение объединений

При присваивании значения объединяющего типа переменной с выведенным типом Scala сохраняет объединяющий тип. Однако некоторые операции расширяют его до наименьшей общей верхней границы, поэтому указывайте тип явно, если хотите сохранить объединение.

object Main:
  def pick(flag: Boolean): Int | String =
    if flag then 1 else "one"

  def main(args: Array[String]): Unit =
    val x: Int | String = pick(false)
    println(x)

Когда что использовать

Выбирайте подходящий инструмент.

  • Объединение: значение имеет один из нескольких типов (OR).
  • Пересечение: значение одновременно удовлетворяет нескольким трейтам (AND).
  • Объединения позволяют избежать типов-обёрток, а пересечения — глубокой иерархии наследования.
trait Logger: def log(s: String): Unit
trait Clock:  def now: Long

object Main:
  def run(env: Logger & Clock): Unit =
    env.log(s"time=${env.now}")

  def main(args: Array[String]): Unit =
    val env = new Logger with Clock:
      def log(s: String) = println(s)
      def now = 100L
    run(env)

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

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

Итоги

Вы изучили объединяющие и пересекающиеся типы Scala 3.

  • A | B: значение является A или B; обёртка и общий родитель не нужны.
  • A & B: значение одновременно является A и B; операция коммутативна.
  • Используйте сопоставление с образцом, чтобы сузить объединения, и объединяйте трейты с помощью пересечений.
  • Используйте псевдонимы type, чтобы давать имена длинным объединениям.
object Main:
  def length(x: String | List[Int]): Int = x match
    case s: String    => s.length
    case l: List[Int] => l.length

  def main(args: Array[String]): Unit =
    println(length("hello"))
    println(length(List(1, 2, 3)))

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

Урок «Типы объединения и пересечения» бесплатный?

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

Чему я научусь в уроке «Типы объединения и пересечения»?

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

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

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

Сколько времени занимает урок «Типы объединения и пересечения»?

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

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

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

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

  1. Значимые отступы
  2. Перечисления в Scala 3
  3. Непрозрачные типы
  4. Типы объединения и пересечения
← Назад к Scala for Backend Engineering & Functional Programming