9.10. Przykłady typów ogólnych

Biblioteka standardowa języka Scala zawiera wiele klas z parametrami typu. Poznajmy kilka z nich. Krotka n-elementowa jest strukturą danych będącą uporządkowanym zbiorem n wartości, przy czym każda z tych wartości może być innego typu. Krotki są instancjami jednej z klas TupleN, gdzie N jest liczbą większą lub równą 2. Język Scala udostępnia specjalną notację dla krotek, polegającą na zapisie elementów krotki oddzielonych przecinkami i objętych w nawiasy okrągłe.

Specyfikacja języka Scala opisuje typy krotek punkcie 3.2.5, krotki w punkcie 6.9, a klasy TupleN w punkcie 12.3.2.

Poniższe przykłady pokazują krotkę dwuelementową składającą się z wartości typu Int oraz wartości typu Symbol (wiersz ), krotkę trójelementową składającą się z dwóch wartości typu Long i jednej typu Double (wiersz ) oraz krotkę pięcioelementową składającą się z pięciu wartości typu Int (wiersz ).

scala> (2,'abc) 
res0: (Int, Symbol) = (2,'abc)

scala> (4L,5L,6.7) 
res1: (Long, Long, Double) = (4,5,6.7)

scala> (1,2,3,4,5) 
res2: (Int, Int, Int, Int, Int) = (1,2,3,4,5)

Do poszczególnych elementów krotek można odwoływać się za pomocą identyfikatorów składających się ze znaku podkreślenia oraz numeru elementu (elementy są numerowane począwszy od 1).

scala> val a = (4, 7L, 'abcd, true)
a: (Int, Long, Symbol, Boolean) = (4,7,'abcd,true)

scala> a._1
res3: Int = 4

scala> a._2
res4: Long = 7

scala> a._3
res5: Symbol = 'abcd

scala> a._4
res6: Boolean = true

Próba odwołania do nieistniejącego elementu powoduje błąd kompilacji.

scala> a._5
<console>:12: error: value _5 is not a member of (Int, Long, Symbol, Boolean)
       a._5
         ^

Krotki są niezmienne. Nie można zmienić wartości elementu istniejącej krotki.

scala> val b = (4, true)
b: (Int, Boolean) = (4,true)

scala> a._1 = 5
<console>:11: error: reassignment to val
       a._1 = 5
            ^

Poniższe przykłady pokazują dwie krotki utworzone bez użycia specjalnej notacji.

scala> new Tuple2[Int,Long](6,8L)
res8: (Int, Long) = (6,8)
scala> new Tuple3(true, 'abc, 2)
res9: (Boolean, Symbol, Int) = (true,'abc,2)

Opcje są obiektami typu Option[+A] i reprezentują wartości opcjonalne. Option jest opieczętowaną klasą abstrakcyjną, z której dziedziczy klasa Some oraz obiekt None. Klasa Some reprezentuje obiekty opcji posiadające wartość, a None reprezentuje obiekty pozbawione wartości. Rysunek 9.1 przedstawia te zależności graficznie.

Rysunek 9.1 — Typ Option[T] i jego podtypy

Plik SqrtOption.scala zawiera definicję metody obliczającej pierwiastek podanej liczby typu Double. Metoda zwraca wartość typu Option[Double].

Plik SqrtOption.scala:
def sqrtOption(x:Double): Option[Double] =
  if (x >= 0) Some(math.sqrt(x)) else None

Jeśli argumentem metody jest liczba nieujemna, wynikiem jest obiekt typu Some[Double] zawierający obliczony pierwiastek, natomiast jeśli jest to liczba ujemna, rezultatem jest obiekt None.

scala> :load SqrtOption.scala
Loading SqrtOption.scala...
sqrtOption: (x: Double)Option[Double]

scala> val option1 = sqrtOption(2)
option1: Option[Double] = Some(1.4142135623730951)

scala> val option2 = sqrtOption(-1)
option2: Option[Double] = None

Za pomocą metody isEmpty można sprawdzić, czy opcja ma wartość (czyli jest instancją klasy Some), czy nie ma (czyli jest obiektem None).

scala> option1.isEmpty
res10: Boolean = false

scala> option2.isEmpty
res11: Boolean = true

W przypadku, gdy opcja jest niepusta, można jej wartość uzyskać za pomocą metody get. Wywołanie metody get na obiekcie None skutkuje wyrzuceniem wyjątku.

scala> option1.get
res12: Double = 1.4142135623730951

scala> option2.get
java.util.NoSuchElementException: None.get
  at scala.None$.get(Option.scala:347)
  at scala.None$.get(Option.scala:345)
  ... 33 elided

Za pomocą metody getOrElse można uzyskać wartość opcji w przypadku, gdy nie jest ona pusta lub wartość domyślną w przeciwnym przypadku.

scala> option1.getOrElse(0.0)
res14: Double = 1.4142135623730951

scala> option2.getOrElse(0.0)
res15: Double = 0.0

Klasa Either jest opieczętowaną klasą abstrakcyjną, która ma dwie podklasy: Left i Right. Konstruktor każdej z podklas ma jeden parametr, ale w każdej z nich ten parametr może być innego typu. Rysunek 9.2 przedstawia te zależności graficznie. Typ Either może być wykorzystywany do reprezentowania rezultatu metody lub funkcji, która może zwrócić wartość jednego z dwóch różnych typów. Istnieje konwencja, według której wartość typu Right reprezentuje rezultat w przypadku prawidłowego wykonania metody lub funkcji, a wartość typu Left reprezentuje rezultat w przypadku błędu.

Rysunek 9.2 — Typ Either[T] i jego podtypy

Plik SqrtEither.scala zawiera definicję metody sqrtEither, w której ta konwencja została zastosowana.

Plik SqrtEither.scala:
def sqrtEither(x: Double):Either[String,Double] =
  if (x >= 0) Right(math.sqrt(x)) else Left("Negative argument: "+x)

W przypadku, gdy argumentem metody jest liczba nieujemna, wynikiem metody jest instancja klasy Right zawierająca wartość typu Double reprezentującą obliczony pierwiastek. W przypadku, gdy argumentem metody jest liczba ujemna, wynikiem metody jest instancja klasy Left zawierająca komunikat błędu w postaci wartości typu String.

scala> :load SqrtEither.scala
Loading SqrtEither.scala...
sqrtEither: (x: Double)Either[String,Double]

scala> val either1 = sqrtEither(2)
either1: Either[String,Double] = Right(1.4142135623730951)

scala> val either2 = sqrtEither(-1)
either2: Either[String,Double] = Left(Negative argument: -1.0)

Za pomocą metod isLeft i isRight można sprawdzić, czy wartość typu Either jest instancją klasy Left, czy Right.

scala> either1.isLeft
res16: Boolean = false

scala> either1.isRight
res17: Boolean = true

scala> either2.isLeft
res18: Boolean = true

scala> either2.isRight
res19: Boolean = false

Za pomocą metod left i right można „rzutować” wartość na lewą lub prawą „stronę”, co pozwala następnie na użycie metod get lub getOrElse służących do uzyskania dostępu do wartości przechowywanej w obiekcie.

scala> either1.left.get
java.util.NoSuchElementException: Either.left.value on Right
  at scala.util.Either$LeftProjection.get(Either.scala:289)
  ... 33 elided

scala> either1.right.get
res21: Double = 1.4142135623730951

scala> either2.left.get
res22: String = Negative argument: -1.0

scala> either2.right.get
java.util.NoSuchElementException: Either.right.value on Left
  at scala.util.Either$RightProjection.get(Either.scala:453)
  ... 33 elided

scala> either1.left.getOrElse("not a left")
res24: String = not a left

scala> either1.right.getOrElse(0.0)
res25: Double = 1.4142135623730951

scala> either2.left.getOrElse("not a left")
res26: String = Negative argument: -1.0

scala> either2.right.getOrElse(0.0)
res27: Double = 0.0

Tablice są typem danych reprezentującym skończone ciągi elementów tego samego typu. Do poszczególnych elementów tablicy o rozmiarze n można się odwoływać za pomocą indeksów o numerach od 0 do n−1. Poniższe przykłady pokazują polecenia tworzące tablicę dwuelementową wartości typu Int (wiersz ), tablicę trójelementową wartości typu Symbol (wiersz ) oraz tablicę pięcioelementową wartości typu Double (wiersz ). W poleceniu tworzącym pierwszą z tych tablic wyspecyfikowano jawnie typy danych tablicy i jej elementów. W pozostałych dwóch poleceniach typy zostały wywnioskowane przez kompilator.

scala> val a: Array[Int] = Array[Int](2,4) 
a: Array[Int] = Array(2, 4)

scala> val b = Array('a,'b,'cde) 
b: Array[Symbol] = Array('a, 'b, 'cde)

scala> val c = Array(1.0,2.3,5.6,78,2.0,-1.3) 
c: Array[Double] = Array(1.0, 2.3, 5.6, 78.0, 2.0, -1.3)

Poszczególne elementy tablicy można odczytywać za pomocą metody apply i zmieniać ich wartości za pomocą metody update.

scala> a(0)
res28: Int = 2

scala> a(1)
res29: Int = 4

scala> b(2)
res30: Symbol = 'cde

scala> b(2) = 'xyz

scala> b
res32: Array[Symbol] = Array('a, 'b, 'xyz)

scala> c(0) = 5.5

scala> c
res34: Array[Double] = Array(5.5, 2.3, 5.6, 78.0, 2.0, -1.3)

Za pomocą metody length można odczytać długość tablicy.

scala> a.length
res35: Int = 2

scala> b.length
res36: Int = 3

scala> c.length
res37: Int = 6

Za pomocą metody ofDim obiektu Array można tworzyć tablice wielowymiarowe. Poniższe polecenia tworzą dwuwymiarową tablicę elementów typu Int oraz zmieniają kilka wartości elementów tej tablicy.

scala> val d = Array.ofDim[Int](2,3)
d: Array[Array[Int]] = Array(Array(0, 0, 0), Array(0, 0, 0))

scala> d(0)(0) = 1

scala> d(1)(0) = 2

scala> d(1)(2) = 5

scala> d
res41: Array[Array[Int]] = Array(Array(1, 0, 0), Array(2, 0, 5))

Specyfikacja języka Scala opisuje klasę Array w punkcie 12.3.4.

Listy są typem danych reprezentującym skończone i niezmienne ciągi elementów tego samego typu. Poniższe przykłady pokazują polecenia tworzące listy o typach, długościach i zawartości podobnych do tablic tworzonych we wcześniejszym przykładzie.

scala> val a: List[Int] = List[Int](2,4)
a: List[Int] = List(2, 4)

scala> val b = List('a,'b,'cde)
b: List[Symbol] = List('a, 'b, 'cde)
scala> val c = List(1.0,2.3,5.6,78,2.0,-1.3)
c: List[Double] = List(1.0, 2.3, 5.6, 78.0, 2.0, -1.3)

Za pomocą metody :: można utworzyć nową listę, złożoną z elementu dopisanego na początku innej listy. Poniższe polecenie tworzy listę trójelementową złożoną z wartości 5 dopisanej na początku listy a. Ta operacja nie zmienia zawartości listy a.

scala> val a1 = 5 :: a
a1: List[Int] = List(5, 2, 4)

scala> a
res42: List[Int] = List(2, 4)

Obiekt Nil reprezentuje listę pustą. Za pomocą tej wartości oraz operacji :: można tworzyć nowe listy. Poniższe polecenia tworzą listy o wartościach takich, jak wcześniej utworzona lista a1. Ponieważ metoda :: wiąże od prawej do lewej, oba polecenia — z nawiasami i bez nawiasów — działają analogicznie.

scala> val a2 = 5 :: (2 :: (4 :: Nil))
a2: List[Int] = List(5, 2, 4)

scala> val a3 = 5 :: 2 :: 4 :: Nil
a3: List[Int] = List(5, 2, 4)

Listy są niezmienne. Można za pomocą metody apply odczytać wartość elementu listy, ale nie ma metody update służącej do zmiany wartości elementu.

scala> a3(1)
res43: Int = 2

scala> a3(1) = 8
<console>:12: error: value update is not a member of List[Int]
       a3(1) = 8
       ^

Strumienie reprezentują (potencjalnie nieskończone) ciągi elementów tego samego typu. W poniższym przykładzie utworzony zostaje strumień elementów typu Int.

scala> val a: Stream[Int] = Stream(1,2,3,4)
a: Stream[Int] = Stream(1, ?)

Poszczególne wartości należące do strumienia nie muszą być obliczone w momencie jego utworzenia. W powyższym przykładzie konsola pokazała wartość pierwszego elementu strumienia, ale resztę elementów zastąpiła znakiem zapytania. Obliczenie wartości wszystkich elementów strumienia można wymusić za pomocą metody force.

scala> a.force
res45: scala.collection.immutable.Stream[Int] = Stream(1, 2, 3, 4)

Za pomocą metody #:: można utworzyć nowy strumień, złożony z elementu dopisanego na początku innego strumienia. Ta operacja nie zmienia zawartości pierwotnego strumienia.

scala> val b = 5 #:: a
b: scala.collection.immutable.Stream[Int] = Stream(5, ?)

scala> a
res46: Stream[Int] = Stream(1, 2, 3, 4)

Dzięki temu, że poszczególne elementy strumienia nie muszą być obliczone przy jego tworzeniu, możliwe jest konstruowanie strumieni nieskończonych. W przykładzie z pliku IntegersStream.scala metoda integers tworzy strumień kolejnych liczb całkowitych rozpoczynając od podanej wartości początkowej. Definicja strumienia jest rekurencyjna — metoda integers wywołuje samą siebie.

Plik IntegersStream.scala:
def integers(n: Int): Stream[Int] = n #:: integers(n+1)

W poniższym wyrażeniu metoda take pobiera określoną liczbę elementów z początku strumienia, a metoda force wymusza obliczenie wartości i pokazanie ich przez konsolę.

scala> :load IntegersStream.scala
Loading IntegersStream.scala...
integers: (n: Int)Stream[Int]

scala> integers(2).take(4).force
res47: scala.collection.immutable.Stream[Int] = Stream(2, 3, 4, 5)

Tablice, listy i strumienie należą do biblioteki kolekcji języka Scala i w związku z tym możliwe jest wywoływanie na instancjach tych klas wielu metod obecnych także w innych rodzajach kolekcji. Informacje na temat kolekcji można znaleźć w rozdziale 18

Język programowania Scala Wydanie 2. Copyright © Grzegorz Balcerek 2016

Licencja Creative Commons

Ten utwór jest dostępny na licencji Creative Commons Uznanie autorstwa-Na tych samych warunkach 4.0 Międzynarodowe.

Oracle and Java are registered trademarks of Oracle and/or its affiliates. Other names may be trademarks of their respective owners.