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.
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.
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 |