Języki

Erudis - your road to knowledge
Scala w akcji. Kontenery w Scali

Gdy już wiemy jak tworzyć wygodnie obiekty reprezentujące dane musimy jeszcze poznać dostępne w Scali sposoby przechowywania obiektów. Do dyspozycji mamy całą gamę różnych klas kontenerowych, przyjrzymy się im dokładniej, bo mają one ciekawe właściwości, a poza tym z praktycznego punktu widzenia są bardzo istotne. Listing 4 zawiera przykłady użycia różnych pojemników na obiekty.

Listing 4. Przykład użycia w Scali tablic i klas kontenerowych

 1  object Collections extends Application {
 2     //tablice
 3     println("* * * tablice * * *")
 4     val tablica: Array[String] = new Array[String](3)
 5     tablica(0) = "Ala" 
 6     tablica(1) = "ma"
 7     tablica(2) = "kota"
 8  
 9     //dostęp swobodny do elementów tablicy
10     print(tablica(0) + " " + tablica(1) + " "  + tablica(2))
11  
12     //for 1: stara, dobra pętla for w wersji Scala
13     for(i <- 0 until 2) 
14       yield print(tablica(i) + " ") 
15     println
16  
17     //for 2: wersja nieco uproszczona
18     for(s <- tablica) 
19       yield print(s + " ")
20     println
21  
22     //foreach: typowo funkcyjne podejście
23     tablica.foreach(s => print(s + " ")) 
24     println
25  
26     //listy
27     println("* * * listy * * *")
28     val lista1 = "ala"::"ma"::"kota"::Nil
29     val lista2 = List("miś", "ma", "uszy")
30     val lista = lista1:::lista2
31     lista.filter(s => s.length <= 3).foreach(println)
32  
33     //tuple
34     println("* * * tuple * * *")
35     import java.util.Date
36     val osoba = ("Miś", "Uszatek", new Date(), 120)
37     println("imię: " + osoba._1 + ", data dodania do bazy: " 
38           + osoba._3 + ", wzrost: " + osoba._4 + "cm")
39  
40     //set
41     println("* * * zbiory * * *")
42     import collection.mutable.HashSet
43     val zbior = HashSet[String]("ala","ma")
44     zbior += "kota"; zbior += "ma"
45     zbior.foreach(s => print(s + " "))
46     println
47  
48     //mapa
49     println("* * * mapy * * *")
50     import scala.collection.mutable.HashMap
51     val mapa = HashMap[Int, String](1->"ala")
52     mapa(2) = "ma"
53     mapa+= 3->"kota"
54     mapa.values.foreach(s => print(s + " "))
55  }

Pierwszą rzeczą, o której trzeba pamiętać, to fakt, że zarówno tablice jak i klasy kontenerowe przechowują obiekty o określonym typie.

Tablice mają zawsze ustaloną wielkość. Dostęp do obiektów w tablicy można zrealizować na kilka sposobów. Pierwszy przykład to prosty dostęp sekwencyjny (Listing 4, linia 10), następnie wykorzystujemy dwie odmiany pętli for, która zgodnie z funkcyjnym charakterem Scali ma nietypową składnię, barwnie zwaną po angielsku For-Comprehensions (linie 13 i 18). Najciekawszy jest ostatni przykład, użycie metody foreach, która jako argument przyjmuje inną funkcję (linia 23). W naszym przypadku jest to tylko print, ale może tam umieścić dowolnie złożoną funkcję, także anonimową, czyli nie mającą nazwy, zdefiniowaną bezpośrednio wewnątrz foreach.

Podobnym do tablicy kontenerem jest lista. Różni się ona od tablicy dwoma bardzo ważnymi cechami: lista nie ma a priori ustalonej wielkości i jest obiektem niezmiennym. Jeśli modyfikujemy listę dodając do niej nowy element, to tak naprawdę tworzymy nowy obiekt reprezentujący powiększoną listę. Poruszanie się poprzez elementy listy jest realizowane podobnie jak dla tablicy, przy pomocy for lub metody foreach. Listing 4 demonstruje także bardzo przydatną metodę, filter, która zwraca listę obiektów spełniających umieszczony w jej argumencie warunek logiczny (linia 31).

Następnym kontenerem jest tupla. Tupla jest bardzo użyteczna, gdyż potrafi przechowywać obiekty różnych typów. Jest to wygodne na przykład wtedy, gdy jakaś metoda musi zwrócić kilka wartości. Programista Java od razu utworzyłby do tego celu klasę JavaBean, tutaj widzimy jak prosto można zbudować tuplę przechowującą dane o osobie (linia 36).

Żadna przyzwoita biblioteka kontenerów nie mogłaby się obejść bez zbioru (Set) i mapy (Map), zwanej czasem tablicą asocjacyjną. Zbiór się charakteryzuje tym, że nie może zawierać duplikatów, ponadto nie zachowuje kolejności dodawania obiektów. Mapa z kolei przechowuje nie pojedyncze obiekty, ale pary klucz – wartość. Oba te kontenery mają dwie wersję – niezmienną oraz modyfikowalną. Nazwy klas w obu przypadkach są identyczne, różni się tylko nazwa pakietu.

Operacje na zbiorze i mapie można wykonywać bardzo wygodnie dzięki przeciążonym operatorom +, -, +=, itp. które pozwalają dodawać lub usuwać obiekty.

Kolekcje niezmienne oraz możliwość przekazywania jako argumentów metod innych metod podkreślają funkcyjną naturę Scali. Programista Javy, C# czy innego języka typowo imperatywnego może odczuwać na początku pewien dyskomfort związany z używaniem zachowujących się na sposób funkcyjny metod foreach czy filter. Przy odrobinie wprawy można przekonać się jednak, że paradygmat programowania funkcyjnego ma swoje zalety i ułatwia pisanie kodu.

Twórcy Scali najwyraźniej wzięli sobie do serca dziesiąte prawo Philipa Greenspuna mówiące, że każdy dostatecznie złożony program w C, Fortranie, itp. zawiera byle jak zaprojektowaną, pełną błędów, wolno działającą implementację fragmentów języka Lisp (lub innego języka funkcyjnego) i postanowili odpowiedni fragment Lispa zaimplementować możliwie poprawnie.