Пересечение типов

Используемый для типов оператор & создает так называемый тип пересечения (intersection type). Тип A & B представляет собой значения, которые одновременно относятся как к типу A, так и к типу B. Например, в следующем примере используется тип пересечения Resettable & Growable[String]:

trait Resettable:
  def reset(): Unit

trait Growable[A]:
  def add(a: A): Unit

def f(x: Resettable & Growable[String]): Unit =
  x.reset()
  x.add("first")

В методе f в этом примере параметр x должен быть как Resettable, так и Growable[String].

Все члены типа пересечения A и B являются типом A и типом B. Следовательно, как показано, для Resettable & Growable[String] доступны методы reset и add.

Пересечение типов может быть полезно для структурного описания требований. В примере выше для f мы прямо заявляем, что нас устраивает любое значение для x, если оно является подтипом как Resettable, так и Growable. Нет необходимости создавать номинальный вспомогательный trait, подобный следующему:

trait Both[A] extends Resettable, Growable[A]
def f(x: Both[String]): Unit

Существует важное различие между двумя вариантами определения f: в то время как оба позволяют вызывать f с экземплярами Both, только первый позволяет передавать экземпляры, которые являются подтипами Resettable и Growable[String], но не Both[String].

Обратите внимание, что & коммутативно: A & B имеет тот же тип, что и B & A.

Общие элементы

Если элемент присутствует и в A, и в B, его тип в A & B является пересечением типа A и типа B. Например, рассмотрим:

trait A:
  def children: List[A]

trait B:
  def children: List[B]

val x: A & B = new C
val ys: List[A & B] = x.children

Тип children в A & B является пересечением типа children в A и его типа в B, то есть List[A] & List[B]. Это может быть дополнительно упрощено до List[A & B], потому что List является ковариантным.

При определении класса C, который наследует A и B, необходимо дать определение метода children с требуемым типом:

class C extends A, B:
  def children: List[A & B] = ???

Пример

trait Book:
  def title: String

trait Audio:
  def track: String

case class AudioBook(title: String, track: String) extends Book, Audio

case class BookAudio(title: String, track: String) extends Audio, Book

trait Library:
  val first: Book
  def items: List[Book]

trait Album:
  val first: Audio
  def items: List[Audio]

class AudiobookLibrary(titles: String*) extends Library, Album:
  private val title: String = titles.head
  val first: Book & Audio = AudioBook(title, "empty")
  def items: List[Book & Audio] =
    titles.map(title => BookAudio("empty", title)).toList

val al = AudiobookLibrary("Пушкин", "Толстой", "Достоевский")
val book: Book = al.first
// book: Book = AudioBook(title = "Пушкин", track = "empty")
println(book.title)
// Пушкин
val books: List[Book] = al.items
// books: List[Book] = List(
//   BookAudio(title = "empty", track = "Пушкин"),
//   BookAudio(title = "empty", track = "Толстой"),
//   BookAudio(title = "empty", track = "Достоевский")
// )
books.foreach(b => println(b.title))
// empty
// empty
// empty
val audio: Audio = al.first
// audio: Audio = AudioBook(title = "Пушкин", track = "empty")
println(audio.track)
// empty
val audios: List[Audio] = al.items
// audios: List[Audio] = List(
//   BookAudio(title = "empty", track = "Пушкин"),
//   BookAudio(title = "empty", track = "Толстой"),
//   BookAudio(title = "empty", track = "Достоевский")
// )
audios.foreach(a => println(a.track))
// Пушкин
// Толстой
// Достоевский

Детали

Правила подтипа

T <: A    T <: B
----------------
  T <: A & B

    A <: T
----------------
    A & B <: T

    B <: T
----------------
    A & B <: T

Из приведенных выше правил можно показать, что & коммутативно: A & B <: B & A для любых A и B.

B <: B           A <: A
----------       -----------
A & B <: B       A & B <: A
---------------------------
       A & B  <:  B & A

Другими словами, A & B - это тот же тип, что и B & A, в том смысле, что эти два типа имеют одинаковые значения и являются подтипами друг друга.

Если C - конструктор типа, то C[A] & C[B] можно упростить, используя следующие три правила:

Когда C является ковариантным, C[A & B] <: C[A] & C[B] можно вывести:

    A <: A                  B <: B
  ----------               ---------
  A & B <: A               A & B <: B
---------------         -----------------
C[A & B] <: C[A]          C[A & B] <: C[B]
------------------------------------------
      C[A & B] <: C[A] & C[B]

Когда C контравариантно, C[A | B] <: C[A] & C[B] можно вывести:

    A <: A                        B <: B
  ----------                     ---------
  A <: A | B                     B <: A | B
-------------------           ----------------
C[A | B] <: C[A]              C[A | B] <: C[B]
--------------------------------------------------
            C[A | B] <: C[A] & C[B]

Стирание типов

Стертый тип для S & T — это стираемый glb (наибольшая нижняя граница) стираемого типа S и T. Правила стирания типов пересечения приведены ниже в псевдокоде:

|S & T| = glb(|S|, |T|)

glb(JArray(A), JArray(B)) = JArray(glb(A, B))
glb(JArray(T), _)         = JArray(T)
glb(_, JArray(T))         = JArray(T)
glb(A, B)                 = A                     if A extends B
glb(A, B)                 = B                     if B extends A
glb(A, _)                 = A                     if A is not a trait
glb(_, B)                 = B                     if B is not a trait
glb(A, _)                 = A                     // use first

См. также TypeErasure#erasedGlb

Связь с составным типом (with)

Типы пересечения A & B заменяют составные типы A with B. На данный момент синтаксис A with B по-прежнему разрешен и интерпретируется как A & B, но его использование в качестве типа (в отличие от предложения new или extends) будет объявлено устаревшим и удалено в будущем.


Ссылки: