Scala — pierwsze kroki cz.3
W poprzednich częściach kursu poznaliśmy kilka podstawowych konstrukcji w Scali. Aby można było operować na jakiś realnych danych i pisać użyteczne skrypty musimy zapoznać się z podstawowymi typami tablicowymi (nazywanymi też kolekcjami). W Scali są one wszystkie typami generycznymi. Oznacza to, że zostały napisane z wykorzystaniem metaprogamowania (generics). Parametryzujemy je podając typ obiektu jaki będą przechowywać. Niektóre z kolekcji jak mapy itp. mogą wymagać określenia większej ilości typów.
Krok siódmy - podstawowe kolekcje
Wszystkie typy kolekcji można podzielić na mutowalne (mutable) czyli takie, których elementy można zmieniać, oraz niemutowalne (immutable). W tych ostatnich nie zmienimy (podmienimy) elementu tablicy na inny, tylko musimy stworzyć nowy obiekt kolekcji sklejając ze starych i nowych elementów. W programowaniu funkcyjnym należy używać te drugie i dlatego pakiet, w których się one znajdują jest domyślnie importowany. Jednak ze względu na to, że Scala ma umożliwiać wykorzystanie kodu Javy , oraz nie narzucać pisania w stylu funkcyjnym dodano również tablice mutowalne, które importujemy z pakietu scala.collection.mutable
Array - tablica stałej długości
Jest to odpowiednik prostej tablicy w Javie. Musimy z góry ustalić jej rozmiar i typ.
val powitanieNapisy = new Array[String](3) powitanieNapisy(0) = "Witaj" powitanieNapisy(1) = "w świecie" powitanieNapisy(2) = "programowania!" println(powitanieNapisy.mkString(" "))
W pierwszej linii przykładu tworzymy tablicę napisów o 3 elementach. O ilości elementów mówi liczba typu Int podana w nawiasach okrągłych. Powstała tablica jest typu Array[String]. Programującym w C++ i Javie od razu rzuci się w oczy sposób zapisu typu tablicy, w którym zamiast nawiasów kątowych < > użyto nawiasów kwadratowych. Wszędzie w metaprogramowaniu oraz kodzie wykorzystującym go, używa się właśnie nawiasów kwadratowych zamiast kątowych. Natomiast gdy chcemy się odwołać do konkretnego elementu tablicy, to zamiast nawiasów kwadratowych, użyjemy nawiasów okrągłych, co widać w 2, 3 i 4 linii. Typy będące kolekcjami mają wbudowane szereg wspólnych wygodnych funkcji, służących do manipulacji nimi, takich jak przedstawiona w ostatniej linii metoda mkString. Metoda ta zamieni całą tablicę w napis i zadba o rozdzielenie go napisem podanym jako parametr tej funkcji, dbając równocześnie, aby nie dodać go na jego końcu.
Przypisanie do kolejnych elementów tablicy może się odbyć za pomocą metody update:
powitanieNapisy.update(0, "Witaj")
Jest to równoważne drugiej linii z pierwszego przykładu. Co więcej linia ta jest w trakcie kompilacji podmieniana właśnie na metodę update. Jest to część mechanizmu, który omówimy w następnym kroku.
Tablicę, której wszystkie elementy są znane na samym początku możemy zadeklarować w następujący sposób:
val tablica = Array("zero", "jeden", "dwa")
Charakterystyczny jest tutaj brak słowa kluczowego new, ponieważ używamy tutaj metody apply napisanej w obiekcie Array (będącej czymś w rodzaju singletonu zespolonego z klasą Array - wyjaśnienie tego pojawi się przy omawianiu klas)
Listy
Jedną z najczęściej używanych kolekcji przy typowym programowaniu w Scali jest typ List. Jest to lista jednokierunkowa. Należy ona do typów niemutowalnych w przeciwieństwie do Array. Polega to na tym, że każdą wartość w tablicy Array można zmienić na inną. Natomiast w List nie. Zamiast podmieniać wartość, tworzymy nową listę łącząc dowolnie różne jej elementy z nowymi, porzucając zbędne. Przypomina to sposób przetwarzania napisów w Javie w klasie String. Aby zadeklarować listę piszemy:
val lista = List(1, 2, 3)
Powstała lista jest typu List[Int] i składa się z 3 elementów.
Łączenie dwóch list i dodawanie elementu do listy:
val lista1 = List(1, 2) val lista2 = List(3, 4) val lista3 = lista1 ::: lista2 val toSamoCoLista3 = lista1 ++ lista2 val lista4 = 5 :: lista3 val lista5 = 1 :: 2 :: 3 :: 4 :: 5 :: Nil
Za każdym razem powstaje nowa lista, a stara pozostaje bez zmiany. W trzeciej i czwartej linii pokazane są dwa alternatywne sposoby tworzenia nowej listy z dwóch innych. Dodawanie elementu z przodu listy realizuje podwójny dwukropek. W ostatniej linii widać jak można w inny sposób stworzyć listę. Ostatni element Nil to pusta lista. List nie posiada metody dodawania elementu na jej końcu. Jest to spowodowane faktem, że w liście jednokierunkowej czas realizacji tej operacji byłby długi i proporcjonalny do ilości elementów. Jeśli potrzebujemy takiej możliwości to musimy użyć innej kolekcji. Jednak w dobrze stosowanym stylu funkcyjnym jest to zazwyczaj zbędne i List jest optymalnym rozwiązaniem. Co najwyżej czasem potrzebujemy użyć metody reverse, której wyniku łatwo się można domyśleć.
Tworzenie List i jej metody
- List() lub Nil - pusta lista
- List(435454L, 34546534L, 24345448548495L) - tworzenie listy z elementami (tutaj typu Long)
- List(1, 2) ::: List(3, 4) - łączenie list i tworzenie nowej (lub ++)
- lista(2) - zwraca 3 element listy (liczone od zera)
- lista.drop(2) - porzuca wszystkie elementy z przodu włącznie z 2 (NIE liczone od zera)
- lista.take(2) - bierze dwie pierwsze elementy listy
- lista.dropRight(2) - podobna do drop tylko porzuca elementy z tyłu
- lista.last - ostatni element listy
- lista.head - pierwszy element listy
- lista.init - wszystkie elementy oprócz ostatniego
- lista.tail - zwraca wszystkie elementy oprócz pierwszego
- val pierwszy :: lista2 = lista - przypisuje pierwszy element listy do zmiennej pierwszy, a resztę do lista2
- lista.reverse - odwraca kolejność elementów w liście
- lista.isEmpty - zwraca prawdę lub fałsz w zależności czy lista jest pusta
- lista.length - zwraca ilość elementów listy
- lista.map(elem => s * 2) - używa funkcji anonimowej do przetworzenia elementów, w tym przypadku zwiększa każdy element 2 krotnie
- lista.mkString(", ") - łączy elementy tworząc napis oddzielony podanym parametrem
- lista.forall(x => x > 3) - zwraca prawdę jeśli wszystkie elementy są większe od 3
- lista.exists(x => x > 3) - zwraca prawdę jeśli chociaż jeden element jest większy od 3
- lista.filter(x => x > 3) - zwraca listę wszystkich elementów większych od 3
- lista.filterNot(x => x > 3) - przeciwny do filter
- lista.foreach(x => printnln(x.toString + " wartość")) - iteruje po wszystkich elementach (niczego nie zwraca)
- lista.sort((x, y) => x > y) - zwraca listę posortowaną w porządku malejącym
Dla przypomnienia: wszystkie te metody nie zmieniają pierwotnej listy i musimy przypisać wynik do nowej listy.
Krotki - tuples
Ten typ danych jest podobny do znanych krotek w Pythonie. Każdy element krotki może być innego typu. Może to być przydatne przy zwracaniu rezultatów przez funkcje i metody, jednak nie należy nadużywać tego mechanizmu, ponieważ kod może stawać się mało czytelny.
val krotka = (3, "trzy", 'a') println(krotka._1) println(krotka._2) println(krotka._3)
Krotka powstaje przez wpisanie danych w nawias, a odwołujemy się do elementu podając jego numer po znaku podkreślenia (_) ale licząc od 1.
Może się wydawać, że ciężko się połapać kiedy liczyć elementy od zera, a kiedy od 1. Jest na to reguła, którą będziemy potrafili stosować kiedy zrozumiemy kiedy dany kod napisany jest w stylu funkcyjnym. Tradycyjnie w językach pochodzących od C używa się numerowania tablic od zera. Natomiast w językach funkcyjnych jak Heskell numeruje się od jeden. Zatem w Scali tam gdzie mamy tradycyjne odwołanie podobne do tablicy liczymy elementy od zera, a tam gdzie użycie jest funkcyjne (jak w metodzie listy drop, take itp.) liczy się od jeden.
Mapy i sekwencje
Jak już wspomniałem, Scala wspiera zarówno programowanie imperatywne jak też funkcjonalne, dlatego posiada wersje kolekcji mutowalnych i niemutowalnych. Jeśli jednak dla pozostałych kolekcji zazwyczaj różnią się one nazwą, to dla sekwecji i map nazywają się tak samo.
Sekwencja zachowuje się podobnie do list jednak jej wewnętrzna implementacja jest inna.
var pojazdy = Seq("samochód", "rower") pojazdy ++ "samolot" println(pojazdy.contains("samolot")
W drugiej linii do kolekcji dodajemy nowy element, jednak sprawdzenie jego obecności da fałsz, ponieważ obiekt pojazdy jest niemutowalny. Musielibyśmy przypisać go z powrotem do pojazdy, co można zrobić jako, że zadeklarowaliśmy go ze słowem kluczowym var.
pojazdy = pojazdy ++ "samolot"
W rzeczywistości obiekty zadeklarowane jako sekwencje są typami pochodnymi od Seq i lepiej mieć kontrolę nad nimi, wybierając samodzielnie ten typ z pominięciem Seq.
Map składa się klucza i wartości, których typ może być dowolny:
import scala.collection.mutable.Map var kurs = Map("USD" -> 3.14, "GBP" -> 5.34) kurs += ("EURO" -> 4.23) kurs("JEN") = 2.34 println(kurs("EURO")*123.34)
Do mutowalnej mapy możemy dodawać pary klucz i wartość (3 i 4 linia), a potem pobierać według klucza. Mapa, tak jak i inne klasy z kolekcji, zawiera też wiele metod wspólnych dla większości kolekcji takich jak map, foreach itd. Można więc operować na niej podobnie do List, a także dzięki wbudowanym metodom, zmienić ją w listę par (krotek).
Był to dość długi krok, ale ważny na drodze do poznania Scali ;)