Blog (30)
Komentarze (5.6k)
Recenzje (0)
@mikolaj_sScala — pierwsze kroki cz.3

Scala — pierwsze kroki cz.3

06.09.2014 00:49, aktualizacja: 06.09.2014 10:19

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

528784

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

528812

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 ;)

Wybrane dla Ciebie
Komentarze (35)