9.8. Typy egzystencjalne
Wyobraźmy sobie, że z jakiegoś powodu nie chcemy lub nie możemy uczynić klas Box i WasteBin odpowiednio kowariantnymi i kontrawariantnymi ze względu na parametry typu. Czy istnieje jakaś możliwość zmiany definicji klasy DrawingChild tak, aby można ją było użyć nie tylko z obiektami typu Box[Paper] i WasteBin[Paper], ale także z obiektami typu Box[ColorPaper] i WasteBin[Any]? Odpowiedź na to pytanie jest twierdząca. Możemy to zrobić przy użyciu tak zwanych typów egzystencjalnych. W deklaracji typu egzystencjalnego występuje słowo kluczowe forSome. Po jego prawej stronie, w nawiasach klamrowych, mogą wystąpić deklaracje typów, które mogą być użyte po jego lewej stronie.
Plik ABC.scala: class A class B extends A class C extends B
Poniższe polecenia pokazują przykład. W przykładzie użyte są klasy A, B i C z pliku ABC.scala tworzące hierarchię dziedziczenia. Typ S jest zdefiniowany jako typ egzystencjalny. Typ S jest dowolnym typem, który spełnia podane w nawiasach klamrowych ograniczenie, a mianowicie, że musi to być podklasa klasy B.
scala> import language.existentialsimport language.existentials scala> type S = X forSome { type X <: B } defined type alias S
![]() | Klauzula importu z wiersza |
Pierwsze z poniższych poleceń nie kompiluje się, gdyż klasa A nie jest podklasą klasy B, a wobec tego instancja tej klasy nie może być przypisana do wartości sa typu S.
scala> val sa: S = new A
<console>:12: error: type mismatch;
found : A
required: S
(which expands to) X forSome { type X <: B }
val sa: S = new A
^
scala> val sb: S = new B
sb: S = B@c28d64
scala> val sc: S = new C
sc: S = C@40454
W kolejnym przykładzie utworzona zmienna rwbc ma typ egzystencjalny i może przyjmować wartości typów ReaderWriter[B] oraz ReaderWriter[C] — mimo że oba typy nie są względem siebie w relacji dziedziczenia — ale nie może przyjąć wartości typu ReaderWriter[A].
scala> var rwbc: ReaderWriter[T forSome { type T <: B }] = new ReaderWriter(new B)
rwbc: ReaderWriter[T forSome { type T <: B }] = ReaderWriter@1478a81
scala> rwbc = new ReaderWriter(new A)
<console>:12: error: type mismatch;
found : A
required: T forSome { type T <: B }
rwbc = new ReaderWriter(new A)
^
scala> rwbc = new ReaderWriter(new C)
rwbc: ReaderWriter[T forSome { type T <: B }] = ReaderWriter@87bfe2
Scala umożliwia skrótowy zapis pewnych rodzajów typów
egzystencjalnych. Zamiast typu T, który po klauzuli forSome
zadeklarowany jest jako type T <: G >: D, gdzie G i D to
odpowiednio ograniczenia górne i dolne, można w deklaracji typu
egzystencjalnego zastosować zapis _ <: G >: D i wtedy można
pominąć deklarację typu T w klauzuli forSome. Jeśli klauzula
forSome jest pusta, to można pominąć samo słowo kluczowe forSome i
nawiasy klamrowe. Oba ograniczenia, górne i dolne, są opcjonalne i
mogą zostać również pominięte. Poniższe wyrażenia pokazują przykłady
takich uproszczeń w wierszach
i
.
scala> var rwab: ReaderWriter[T forSome { type T >: B }] = new ReaderWriter(new A)
rwab: ReaderWriter[T forSome { type T >: B }] = ReaderWriter@d10c1a
scala> var rwab: ReaderWriter[_ >: B] = new ReaderWriter(new A)rwab: ReaderWriter[_ >: B] = ReaderWriter@1dabb18 scala> rwab = new ReaderWriter(new B) rwab: ReaderWriter[_ >: B] = ReaderWriter@245eb0 scala> var rwabc: ReaderWriter[U forSome { type U }] = new ReaderWriter(new A) rwabc: ReaderWriter[U forSome { type U }] = ReaderWriter@5b8a71 scala> var rwabc: ReaderWriter[_] = new ReaderWriter(new A)
rwabc: ReaderWriter[_] = ReaderWriter@1e61854 scala> rwabc = new ReaderWriter(new B) rwabc: ReaderWriter[_] = ReaderWriter@e34f77 scala> rwabc = new ReaderWriter(new C) rwabc: ReaderWriter[_] = ReaderWriter@2eef62
![]() | Uproszczone zapisy typów egzystencjalnych, takie jak użyte w wierszach
|
Plik ExistentialDrawingChild.scala zawiera definicję klasy ExistentialDrawingChild, która jest kolejną wersją klasy modelującej rysujące dziecko.
Plik ExistentialDrawingChild.scala: class ExistentialDrawingChild(name: String, box: Box[_ <: Paper], wasteBin: WasteBin[_ >: Paper]) { def draw = { val paper = box.take println("Drawing on "+paper) wasteBin.throwAway(paper) } override def toString = name }
Parametry box i wasteBin są zdefiniowane jako typy egzystencjalne, zapisane w notacji skróconej. W przypadku parametru box akceptowane wartości argumentów powinny być typu Box sparametryzowanego typem, dla którego Paper jest ograniczeniem górnym. W przypadku parametru wasteBin akceptowane wartości argumentów powinny być typu WasteBin sparametryzowanego typem, dla którego Paper jest ograniczeniem dolnym. Klasa ExistentialDrawingChild może być użyta z argumentami typu Box[ColorPaper] i WasteBin[Any], co pokazują poniższe przykłady.
scala> val yellowPaperBox = new Box[ColorPaper](
| "yellow paper box", new ColorPaper("yellow"))
yellowPaperBox: Box[ColorPaper] = yellow paper box
scala> val universalWasteBin = new WasteBin[Any]("universal waste bin")
universalWasteBin: WasteBin[Any] = universal waste bin
scala> val ann = new ExistentialDrawingChild(
| "Ann", yellowPaperBox, universalWasteBin)
ann: ExistentialDrawingChild = Ann
scala> ann.draw
Taking yellow paper from the yellow paper box
Drawing on yellow paper
Throwing away yellow paper into the universal waste bin
Jeśli w deklaracji typu egzystencjalnego użyto deklaracji type T
<: S with Singleton, to można ją zastąpić deklaracją val x: S. W
pliku ExistentialDrawingChild2.scala znajdują się dwie klasy o
podobnym działaniu, które w różny sposób deklarują typ parametru box
(wiersze
i
). W wierszu
jest użyty zapis wykorzystujący słowo
kluczowe val. Dla uproszczenia w definicjach tych klas nie jest
używany parametr wasteBin.
Plik ExistentialDrawingChild2.scala: object RedPaper extends ColorPaper("red") object RedPaperBox extends Box("red paper box", RedPaper) class ExistentialDrawingChild2a(name: String, box: Box[T] forSome { type T <: RedPaper.type}) {
def draw = { val paper = box.take println("Drawing on "+paper) } override def toString = name } class ExistentialDrawingChild2b(name: String, box: Box[x.type] forSome {val x: RedPaper.type}) {
def draw = { val paper = box.take println("Drawing on "+paper) } override def toString = name }
![]() | Specyfikacja języka Scala opisuje typy egzystencjalne w punkcie 3.2.10. |
Poniższe przykłady ilustrują działanie obu klas. Ostatni przykład nie kompiluje się z powodu niezgodności typu argumentu z deklaracją parametru.
scala> :load ExistentialDrawingChild2.scala
Loading ExistentialDrawingChild2.scala...
defined object RedPaper
defined object RedPaperBox
defined class ExistentialDrawingChild2a
defined class ExistentialDrawingChild2b
scala> val helenA = new ExistentialDrawingChild2a("HelenA", RedPaperBox)
helenA: ExistentialDrawingChild2a = HelenA
scala> helenA.draw
Taking red paper from the red paper box
Drawing on red paper
scala> val helenB = new ExistentialDrawingChild2b("HelenB", RedPaperBox)
helenB: ExistentialDrawingChild2b = HelenB
scala> helenB.draw
Taking red paper from the red paper box
Drawing on red paper
scala> val susan2 = new ExistentialDrawingChild2b("Susan", yellowPaperBox)
<console>:14: error: type mismatch;
found : Box[ColorPaper]
required: Box[_ <: RedPaper.type with Singleton]
Note: ColorPaper >: RedPaper.type, but class Box is invariant in type T.
You may wish to define T as -T instead. (SLS 4.5)
val susan2 = new ExistentialDrawingChild2b("Susan", yellowPaperBox)
^
Plik ABC.scala:
class A
class B extends A
class C extends B
import language.existentials
scala> type S = X forSome { type X <: B }
defined type alias S


