Scala Tutorial

More...

Scala Tutorial: Wstęp

Ostatnia aktualizacja z: 2020-07-16

Scala Tutorial będzie powstawał w sposób sukcesywny i niezbyt szybko. Mniej więcej jeden wpis na tydzień. Jest to związane z dużą ilością przypadających na mnie obowiązków. W wersji ostatecznej powinien zapewnić wiedzę potrzebną do rozpoczęcia własnej pracy ze Scalą.

Miłej zabawy.

01. Narzędzia

Scastie i ScalaFidle

Czasami możesz potrzebować na szybko przetestować mały kawałek kodu. Może Ci się nie chcieć odpalać do tego celu całego IDE. Na szczęście jest kilka sposobów, by to zrobić w inny (prostszy) sposób.

Pierwszy z nich to skorzystanie ze Scala REPL, napiszę o tym więcej w niedalekiej przyszłości (prawdopodobnie już za tydzień). Można też skorzystać z dobrodziejstw projektów online.

‌Znam dwa narzędzia, które mogę Ci polecić. 

Pierwszy z nich to Scastie, drugi to ScalaFiddle.

Zarówno usługa Scastie, jak i ScalaFiddle umożliwiają uruchamianie kodu Scali bezpośrednio z poziomu przeglądarki.

Pozwól, że pokrótce przedstawię Ci możliwości Scastie.

Podczas pierwszego uruchomienia narzędzie przywita Cię ekranem powitalnym, wyjaśniającym podstawową funkcjonalność. 

Jak widzisz, można edytować i uruchamiać kod Scali, zmieniać wersje kompilatora i bibliotek, lub wręcz dodawać nowe biblioteki. Można również przy użyciu code snippets (mając konto na GitHubie) zapisać swoją pracę i udostępniać ją innym bezpośrednio w Scastie.

Jeśli jesteś po lekturze "Scala w 60 minut" to wiesz, że wszystkie zawarte w książce przykłady udostępniłem właśnie w ten sposób.

Możemy w oknie przeglądarki wpisać np:

val a = 12
println(a)

I kliknąć przycisk Save, albo z klawiatury ctrl+S aby skompilować i uruchomić powyższy kod.

Jak widać w konsoli (którą można ukryć / pokazać przy pomocy klawisza F3) pojawił się wynik skryptu.

Bardzo istotne jest zwrócenie uwagi na przycisk Worksheet

Jeśli go klikniemy, znajdująca się obok kropka zmieni kolor na czarny, a Scastie przejdzie w  tryb wykonywania programów, a nie skryptów (wymóg istnienia startowej funkcji main). 

Co się stanie jeśli spróbujesz uruchomić nasz prosty skrypt z wyłączonym trybem Worksheet?

To wszystko, narzędzie jest dosyć proste i myślę, że bez problemów sobie z nim poradzisz.  ScalaFiddle działa na podobnych zasadach i chyba nie potrzebuje dodatkowych wyjaśnień.

Scala REPL

Scala podobnie jak np. Python daje  możliwość uruchamiania i testowania kodu z poziomu prostego konsolowego interaktywnego narzędzia.

Scala REPL uruchamiamy po prostu uruchamiając komendę scala.

Można z jej poziomu importować moduły, definiować funkcje i testować je. Jest to doskonałe narzędzie do szybkiego przetestowania wszelkich pomysłów.

Jak potężne jest to narzędzie, niech poświadczy David Pollak, który całą swoją książkę (doskonałą btw) Begining Scala oparł na przykładach uruchamianych właśnie z poziomu Scala REPL.

Jest to jednak narzędzie oględnie rzecz ujmując toporne w codziennej pracy 🙂 Jego podstawową wadą jest próba uruchomienia każdej wprowadzonej linii kodu. Nie da się np wprowadzić takiego kawałka kodu:

if (true)
    12
else
    10

W tym konkretnym przypadku można to obejść, używając nawiasów klamrowych:

if (true) {
    12
} else {
    10
}

Nie jest to jednak rozwiązanie najwygodniejsze, poza tym nie w każdym przypadku możemy je zastosować.

Na szczęście można to obejść.

Pierwsza metoda to użycie trybu :paste w trybie tym możemy wprowadzić dowolny fragment kodu np. przez wstawienie go ze schowka, lub wpisanie ręczne, ale uwaga, zwróć uwagę, na to, że po tak pisanym kodzie nie da się nawigować, więc nie da się poprawić błędów. 

Druga metoda polega na użyciu komendy :load, która jak można się domyślić umożliwia załadowanie kodu z pliku.

Zwróć jednak uwagę na to, żeby plik który chcesz wczytać, nie był elementem pakietu (nie zaczynał się od deklaracji package), ponieważ Scala REPL zwróci w takim przypadku kod błędu. 

Można to jednak obejść, kompilując kod narzędziem zewnętrznym, a następnie korzystając z takiej biblioteki bezpośrednio w Scala REPL. Biblioteka musi być jednak widziana przez Scala REPL czyli znajdować się w tzw. classpath. Można to uzyskać między innymi startując REPLa z przełącznikiem -cp

scala -cp nazwabiblioteki.jar

Można to też zrobić już po uruchomieniu REPLa przy pomocy komendy :cp

:cp nazwabiblioteki.jar

Jeśli chcesz dowiedzieć się więcej na temat Scala REPL możesz skorzystać z komendy :help.

Wstrzymaj się z tym jednak w tym momencie, poczekaj do kolejnej lekcji, bo być może wcale tutaj nie wrócisz.

Minusem Scala REPL, jest to, że jest to narzędzie… Najogólniej rzecz mówiąc toporne. Dlatego też w kolejnej lekcji przedstawię moim zdaniem bardzo interesującą (i dużo nowocześniejszą) alternatywę.

Ammonite REPL

Ammonite to grupa projektów tworzonych przez Li Haoyi.

W skład grupy narzędzi wchodzą:

  • Scala Scripts - narzędzie które umożliwia bardzo wygodne uruchamianie skryptów (np. bezpośrednio z basha)
  • Ammonite Ops - biblioteka, mocno ułatwiające pracę z filesystemem 
  • Ammonite Shell - Scalowy następca Basha 😉
  • Ammonite REPL - na którym skoncentrujemy się w tym rozdziale. Jest on zdecydowanie bardziej podobny do IPythona, czy konsoli Zsh, niż do scala REPL.

Zgodnie z instrukcją ze strony Ammonite REPL instalujemy go przy pomocy curla. 

$ sudo sh -c '(echo "#!/usr/bin/env sh" && curl -L https://github.com/lihaoyi/Ammonite/releases/download/1.8.1/2.13-1.8.1) > /usr/local/bin/amm && chmod +x /usr/local/bin/amm' && amm

Następnie uruchamiamy przy pomocy komendy

amm

Przedstawię tylko kilka podstawowych możliwość Ammonite REPL i pozwolę Ci samemu odkryć pełnię jego możliwości (o ile tylko oczywiście będziesz mieć na to ochotę).

Już na pierwszy rzut oka po uruchomieniu widać, że obraz jest bardziej kolorowy. Rezultat wykonanych operacji również jest przedstawiony dużo ładniej.

Seq.fill(10)(Seq.fill(3)("Foo"))

Pamiętaj, że wynik działania jest obcinany do pewnej długości. Jeśli zależy Ci na wyświetleniu całości należy użyć komendy

show(...)

Np wykonanie komendy:

Seq.fill(20)(100)

Wyświetli tylko część wyniku. Jeśli chcesz zobaczyć całość:

show(Seq.fill(20)(100))

Ammonite podświetla składnie i pozwala poruszać się po edytowanym kodzie, czego brakowało w Scala REPL, jednak podobnie jak w Scala REPL naciśnięcie ENTER powoduje próbę wykonania kodu.

W celu wprowadzenia więcej niż jednej linii kodu należy użyć nawiasów klamrowych

{
  val x = 1
  val y = 2
  x + y
}

Po naciśnięciu Entera część kodu w nawiasach klamrowych zostanie z nich niejako wyciągnięta i wykonana. Warto jednak pamiętać, że czasami możesz chcieć wprowadzić nawiasy klamrowe jako blok kodu Scali, w takiej sytuacji należy użyć podwójnych nawiasów.

{{
  val x = 1
  val y = 2
  x + y
}}

Narzędzie to pozwala również na operowanie na historii operacji (strzałki góra i dół), oraz wyszukanie operacji historycznych po kliknięciu ctrl + R.

W swoich rękach masz również undo (ctrl - ) oraz redo (alt / esc)

Narzędzie jest bardzo rozbudowane i pozwala na bardzo wygodną pracę. Jeśli chcesz dowiedzieć się więcej na jego temat zalecam zapoznanie się z dokumentacją, która swoją drogą jest naprawdę bardzo dobra. 

Budujemy aplikację

Poniżej przedstawiam linki, do 3 najczęściej używanych narzędzi budowania aplikacji dla Scali (w kolejności popularności).

Nie wywodzę się ze środowiska Javovego, więc cząsteczki Mavena nie płyną w mojej krwi, gdy powstał Gradle już od dawna korzystałem z sbt i nie planuje zmieniać takiego stanu rzeczy, więc bardzo mi przykro, ale nie powiem Ci nic na temat tych dwóch pozostałych narzędzi.

Skoncentrujemy się na sbt, jako głównym (i zdecydowanie najpopularniejszym) narzędziu do budowania aplikacji Scalowych.

Oczywiście, jeśli wolisz używać znanych sobie rozwiązań, nie mam nic przeciwko, ale po pierwsze nie będę w stanie pomóc w razie problemów, po drugie obawiam się, że w przypadku dużego projektu można w pewnym momencie trafić na ścianę, której obejście będzie skomplikowane. Pluginy sbt bardzo ułatwiają życie. 

Jeśli sbt Ci się nie spodoba, to nie koniec świata. IDE którego będziemy używać w tym tutorialu, nadbudowuje praktycznie wszystko, co jest związane z SBT i ściąga z Twoich barków cały ciężar ręcznej konfiguracji (tak długo, jak nie chcesz zrobić czegoś naprawdę mocno niestandardowego).

Na pewno, warto wiedzieć, jak pracować z sbt i co sbt potrafi, ponieważ wiedza ta zaowocuje w przyszłości, jeśli zajmiesz się dużymi projektami.

Sbt tak samo, jak scala repl uruchamia się z linii komend i jest to interaktywna aplikacja konsolowa.

Możesz zresztą z jej poziomu uruchomić scalę, wpisując komendę console. Co istotne, będziesz mieć w takiej sytuacji załadowane wszystkie moduły tworzone w ramach Twojego projektu, co mocno ułatwia testowanie.

Proponuję Ci teraz zapoznanie się ze stroną dokumentacji sbt, aby uzyskać pełen obraz tego narzędzia i jego poziomu komplikacji.

Rozwiązanie to jest bardzo potężne, ale niestety czasami również mocno nieintuicyjne.

W kolejnych rozdziałach dowiesz się więcej na ten temat i wykonasz kilka praktycznych ćwiczeń.

SBT

SBT to będzie nasze główne narzędzie do budowania projektu. Zrozumiem, jeśli Cię nie zachwyci jego złożona konfiguracja. 

Zaprezentuję Ci teraz podstawy tego narzędzia. W jednym z późniejszych rozdziałów dowiesz się o SBT dużo więcej.

Pamiętaj, jeśli sbt Ci się nie spodoba, to nie koniec świata. IDE którego będziemy używać w czasie tego Tutoriala, nadbudowuje praktycznie wszystko co jest związane z SBT i ściąga z Twoich barków cały ciężar ręcznej konfiguracji (tak długo, jak nie chcesz czegoś naprawdę mocno niestandardowego zrobić).

W każdym razie moim zdaniem, warto wiedzieć, jak pracować z sbt i co sbt potrafi, ponieważ wiedza ta na pewno zaowocuje w przyszłości jeśli zajmiesz się dużymi projektami.

Sbt tak samo jak scala repl uruchamia się z linii komend (wpisując po prostu sbt) i jest to interaktywna aplikacja konsolowa.

Możesz zresztą z jej poziomu uruchomić scalę, wpisując komendę console.

Co istotne, będziesz mieć w takiej sytuacji załadowane wszystkie moduły tworzone w ramach Twojego projektu co mocno ułatwia testowanie. 

‌Proponuję, teraz udać się choć na chwilę na stronę dokumentacji sbt. Taka krótka wycieczka pozwoli Ci ocenić jak rozbudowane jest to narzędzie. Nie przejmuj się tym jednak w tym momencie. Do wszystkiego co najistotniejsze prędzej czy później dojdziemy w dalszej części tego tutoriala.

Przyspieszacze

Tak jak wspomniałem we wstępie na temat budowania aplikacji, kompilator Scali jest wolny.

Jest co prawda o wiele szybszy, niż był jeszcze parę lat temu, ale wciąż, nie jest to poziom, zadowalający, szczególnie jeśli Twój produkt szybko się rozrasta.

Oczywiście znaleźli się ludzie, którzy postanowili zmienić ten stan rzeczy. I tak właśnie powstała Hydra.

Jak to działa?

Kompilator Scali dzieli swoją pracę na kilkanaście etapów i każdy z nich przechodzi sekwencyjnie. W dodatku używa do tego tylko jeden (!!!) rdzeń procesora.

Biorąc pod uwagę, że w dzisiejszych czasach nawet telefony komórkowe mają ich więcej, jest to delikatnie rzecz mówiąc, szczególnie irytujący element kompilatora.

Panowie z Hydry postanowili zmienić ten stan rzeczy, przez równoległe kompilowanie wielu porcji kodu (na które wcześniej Hydra dzieli kompilowany kod). Co też obrazuje ładna animacja.

Niestety, narzędzie jest dosyć drogie i prawie na pewno nie znajdzie zastosowania w twoich prywatnych eksperymentach ze Scalą. Jednak do celów firmowych, gdy kodu jest coraz więcej, może być ciekawą alternatywą.

Drugim narzędziem, które ma za zadanie przyspieszenie pracy ze Scalą (oraz pracy samych aplikacji Scalowych) jest GraalVM. Jest to specjalna maszyna virtualna zaprojektowana do szybszego wykonywania kodu wielu różnych języków programowania, w tym Javy i Scali.

Jest to dosyć nowe narzędzie, a oficjalne wsparcie dla Scali jest raptem od 11 czerwca, więc traktuje je bardziej jako ciekawostkę, ale taką, o której warto wiedzieć. Sam nie miałem z nim wcześniej styczności, poczekam na większą dojrzałość tego projektu. Nie jest wykluczone, że GraalVM stanie się bohaterem jednej z późniejszych lekcji bonusowych.

Jest jeszcze jeden tool, który traktuję bardziej jako ciekawostkę, ale czasami może się okazać przydatny. Mowa o kompilatorze Scali do kodu maszynowego czyli o Scala Native. Nie wszystko da się w ten sposób skompilować i nie wszystko będzie działało np. w chwili obecnej nie ma obsługi wielowątkowości, czyli nie zadziałają również popularne Futures.

Nie zajmowałem się nigdy tym rozwiązaniem, wiem, że jest i jeśli będę potrzebował wygenerować kod natywny ze Scali będzie można spróbować. Chociaż nie jestem przekonany czy w takiej konkretnie sytuacji nie użyję jednak innego języka programowania z kompilatorem, do którego mam pełne zaufanie.

Jakie IDE

Do pracy ze Scalą możemy używać wielu różnych środowisk.

Poniżej przedstawia kilka alternatyw:

Jeśli jesteś fanem, któregoś z tych rozwiązań, nie ma problemu możesz się "męczyć" dalej 🙂

Jednak ze swojej strony proponuję inne narzędzie. Jakie? Ci którzy mnie znają już na pewno świetnie wiedzą, Ci, którzy mnie nie znają dowiedzą się już a momencik.

I nie… Nie będzie to notatnik 🙂


IntelliJ IDEA - Scala Plugin

IntelliJ IDEA to najlepsze IDE, z jakim miałem okazję kiedykolwiek pracować. Jest niesamowicie potężne i bardzo wygodne.

I jak na swoje możliwości tanie. A nawet bardzo tanie, biorąc pod uwagę iż plugin Scali można uruchomić w wersji Community która jest całkowicie darmowa.

T‌utaj link do listy różnic pomiędzy wersją Community a Ultimate (płatną).

Wróćmy jednak do głównego tematu rozdziału czyli pluginu do Scali.

Gdy wiele lat temu Martin Odersky (twórca Scali), wypuścił na Courserze swój kurs Scali, jako preferowane IDE był określony Eclipse. Z tego co pamiętam, Martin współpracował wówczas z ludźmi którzy odpowiadali za stworzenie obsługi Scali w tym IDE.

W trakcie kursu okazało się jednak, że jak jeden mąż wszyscy kursanci porzucili Eclipse na rzecz IntelliJ IDEA, ponieważ tutaj obsługa Scali była na nieporównywalnie wyższym poziomie i tak pozostało do dnia dzisiejszego.

Plugin Scali jest nieustannie rozwijany, każde kolejne wydanie IntelliJ IDEA jest równoznaczne z wydaniem nowej wersji tego pluginu. 

Nie chciał bym tutaj teraz opisywać samego IntelliJ IDEA, ponieważ jest duża szansa iż było przez Ciebie już wcześniej używane.

Więc teraz tylko kilka najistotniejszych informacji.

Interfejs użytkownika ma dosyć standardowy podział. Oczywiście większość okienek można odłączyć i traktować jako niezależne byty, co sam robię często z konsolą SBT gdy przenoszę ją na 2 monitor, by mieć stały podgląd np. na testy.

Standardowo jest uruchomiony tryb analizy składni kodu, co jest oznaczone małą ikonką [T] znajdującą się w prawym dolnym rogu. Analizę można wyłączyć przy pomocy kombinacji ctrl + shift + alt + e.


Okno SBT można uruchomić przy pomocy View | Tool Windows, lub korzystając z wyszukiwarki poleceń Ctrl+Shift+A

Z podstawowych możliwości jakie ma IDEA warto pamiętać o doskonałej obsłudze lokalnej historii.

Oraz świetnej integracji z systemami kontroli wersji, aczkolwiek tutaj mogę się wypowiadać wyłącznie o integracji z gitem.

Dopełnianie kodu.

Masz podstawowe dopełnienie (basic completion), uruchamiane przez ctrl+space, ponowne kliknięcie ctrl + space doda więcej rezultatów, masz również smart completion (ctrl + shift + space) które bazuje na kontekście, tutaj również, ponowne użycie skrótu da więcej rezultatów. 

Jest jeszcze Statement completion które automatycznie dopisze brakujące nawiasy i sformatuje kod.

Podpowiedź parametrów do wywołania metod czy konstruktorów można łatwo uzyskać przy pomocy ctrl + p.

Jeszcze kilka niezwykle przydatnych skrótów.

Szybkie dwukrotne naciśnięcie klawisza shift - pozwala włączyć okno szybkiego wyszukiwania wszędzie w kodzie. BARDZO potężne narzędzie.


Podgląd dokumentacji ctrl+Q


Szybkie wyświetlenie definicji ctrl + shift + i


Wyszukanie użycia np. zmiennej, czy metody przy pomocy ctrl + alt + F7, zmiana nazwy zmiennej, czy metody (w ramach refaktoryzacji) shift + F6

Ahh i na samym końcu mój ulubiony skrót ctrl+shift+backspace który umożliwia na szybkie nawigowanie po ostatnio zmienianych elementach kodu.

Poniżej znajdziesz listę najważniejszych moim zdaniem skrótów, które BARDZO ułatwiają codzienną pracę z IntelliJ IDEA.


Najważniejsze skróty

  • ctrl+shifl+alt+E - wyłączenie analizy składni
  • ctrl+shift+A - wyszukiwarka poleceń
  • ctrl+shift+F12 - włączenie / wyłączenie widoczności okien narzędzi
  • shift x 2 - szybkie wyszukiwanie wszędzie
  • ctrl+Q - podgląd dokumentacji
  • ctrl+shift+I - szybkie wyświetlenie definicji
  • ctrl+alt+F7 - wyszukanie użycia np. zmiennej, czy metody
  • shift+F6 - zmiana nazwy zmiennej, czy metody (w ramach refaktoryzacji)
  • ctrl+shift+backspace - szybkie nawigowanie po ostatnio zmienianych elementach kodu
  • Ctrl+Alt+S - ustawienia

Code completion

  • ctrl+space - podstawowe dopełnienie (klikniecie ponowne, da więcej rezultatów)
  • ctrl+shift+space - sprytne dopełnienie, bazujące na kontekście (tutaj również ponowne kliknięcie da więcej rezultatów)
  • ctrl+shift+enter - dopełnienie wyrażenia, które doda automatycznie odpowiednie nawiasy i formatowanie
  • ctrl+P - podpowiedzi parametrów do metod czy konstruktorów

Okna narzędzi

  • alt + 1 - okno projektu
  • alt + 9 - Version control
  • alt + 4 - run
  • alt + 5 - debug
  • alt + F12 - terminal
  • esc - edytor

Podstawowe operacja edytora

  • Ctrl+Shift+Up, Ctrl+Shift+Down - przeniesienie aktualnej linii kodu
  • Ctrl+D - kopia aktualnej linii kodu
  • Ctrl+Y - usuń linijkę kodu
  • Ctrl+/ - skomentowanie / odkomentowanie linii
  • Ctrl+Shift+/ - skomentuj blok kodu
  • Ctrl+F - znajdź w aktualnym pliku
  • Ctrl+R - znajdź i zastąp w aktualnie otwartym pliku
  • F3 - następne wystąpienie
  • Shift+F3 - poprzednie wystąpienie
  • Alt+Right, Alt+Left - chodzenie po zakładkach
  • Ctrl+NumPad+PLUS, Ctrl+NumPad+MINUS - zwinięcie, rozwinięcie bloku kodu
  • Alt+Insert - generowanie
  • Ctrl+Alt+T - otocz
  • Ctrl+F7 - podświetl użycie symbolu

Po więcej skrótów zapraszam na stronę Scala Plugin

IntelliJ IDEA - Worksheet

Pisałem Ci już o Scastie, ScalaFidle, Scala REPL i Ammonite REPL, czyli miejsach gdzie można szybko przetestować jakiś kawałek kodu. 

W IntelliJ IDEA najłatwiej to zrobić korzystając z tzw. Worksheet.

Utwórz sobie nowy plik rodzaju Scala Worksheet, i nadaj mu nazwę np. test_worksheet.

val x = 12
val y = 14
x + y

Uruchom ten prosty kawałek kodu albo przez kliknięcie ikonki z zielonym przyciskiem Play, albo przez ctrl+enter.

Ekran podzielił się na dwie kolumny. W kolumnie pierwszej widać wpisany kod, natomiast w kolumnie drugiej widać wynik jego działania, linijka po linijce.

Widać, że zmienne są typu Int, że rezultat sumy x+y również jest Intem i że suma wynosi 26

Możesz jednak skonfigurować nasze IDE, tak, aby do pracy z worksheetem używało Ammonite, zamiast standardowego rozwiązania.

Wejdź w konfigurację (ctrl+alt+S), w Languages & Frameworks, Scala, Zakładka Worksheet i zmieniamy "Treat .sc files as: "Always Ammonite".

Teraz ctrl+enter przestał działać, a po najechaniu myszką na przycisk play pojawia się dodatkowa opcja "run script".

Twój kawałek kodu jest traktowany jak skrypt do uruchomienia. Dopisz do niego zatem jedną dodatkową linijkę, żeby sprawdzić czy tak faktycznie jest:

println(x+y)

Jak widzisz po uruchomieniu nie ma już podziału na 2 okna. Zato w oknie wyników, zobaczysz wypisany rezultat działania czyli liczbę 26.

Metodę działania Worksheet możesz zmienić w dowolnym momencie w zależności od tego co będzie Ci potrzebne.

Skąd brać biblioteki

Są dwa takie miejsca, gdzie można wyszukiwać bibliotek na potrzeby twojego projektu w Scali.

Wejdź sobie najpierw na stronę Scaladex. Wyszukaj np. akka-actor.

Po znalezieniu po prawej stronie ekranu widzisz panel zakładek, w których znajdziesz kod potrzebny do użycia biblioteki w konkretnych budowaczach. I tak np. dla sbt znajdziesz następujący wpis (lub podobny, gdy zmieni się wersja aktualnej biblioteki).

libraryDependencies += "com.typesafe.akka" %% "akka-actor" % "2.6.0"

Teraz sprawdź jak to zadziała w wyszukiwarce mavenowej

Już na pierwszy rzut oka, widać, że wyszukiwarka ta (chodź z wyglądu bardziej "toporna") działa o wiele szybciej.

Wybierz ostatnią stabilną wersję (jest to o tyle istotne, że czasami możemy poszukiwać starszych wersji bibliotek - co jest nie możliwe w Scaladex), przejdź na zakładkę sbt i uzyskasz identyczny rezultat co poprzednio:

libraryDependencies += "com.typesafe.akka" %% "akka-actor" % "2.6.0"

Co z taką linijką zrobisz dowiesz się w jednym z kolejnych rozdziałów. Tym razem chciałem Ci tylko pokazać jak najłatwiej szukać potrzebnych bibliotek.

Sam od zawsze używam mvnrepository, ponieważ gdy zaczynałem nie było jeszcze Scaladexa, czasami trzeba było szukać informacji na stronach bibliotek, żeby się dowiedzieć, jaka faktycznie biblioteka powinna być użyta i jak dodana do sbt, ponieważ w mvn były stare informacje.

W‌arto pamiętać, że Scaladex bazuje wyłącznie na projektach z githuba, ale za to obejmuje tylko projekty Scalowe, nie widziałem tam jednak odnośników do innych wersji bibliotek, poza aktualną, a to czasami może mocno utrudnić życie.

Na stronie Scaladex można znaleźć informacje jak dodać swój projekt do tej wyszukiwarki, może Cię to zainteresuje.

02. Podstawy i testy

W tym rozdziale opowiem Ci o podstawach Scali oraz o mechanizmie który będzie nam bardzo potrzebny w czasie całego tego tutoriala. Z jednej strony do oceniania Twoich postępów, a z drugiej umożliwi Ci sprawdzanie, kodu który napiszesz.

Zapewne domyślasz się już iż porozmawiamy co nieco o tworzeniu testów. Przedstawię Ci kilka możliwych narzędzi których można użyć do tworzenia testów, pokażę z którego sam korzystam i który rekomenduje Tobie.

Napiszesz sobie kilka prostych testów dzięki czemu przygotujesz się na ciężką pracę w kolejnych rozdziałach.

Założenia początkowe

Żebym mógł pójść dalej, przedstawię Ci na początek kilka absolutnie podstawowych informacji które będą przydatne podczas całego tego tutoriala.

Jest duża szansa, że już to wszystko wiesz, ale zamiast zakładać, że tak jest wolę mieć pewność 🙂

  • = określa przypisanie wartości
  • == to test równości
  • != kontrola czy wartości po obu stronach są różne od siebie
  • znak ! to negacja (czyli inaczej not)
  • || - or
  • && - and
  • assert(warunek, tekst) - prosta komenda testująca. Jeśli warunek jest spełniony, nic się nie dzieje, w przeciwnym wypadku generowany jest exception (z opisem pobranym z argumentu tekst).

Stałe, zmienne, lazy i średniki

Zajmiemy się teraz podstawowymi informacjami o stałych i zmiennych. Postaram się to zrobić jak najszybciej, ale proszę nie pomijaj tej lekcji bo znajdzie się tutaj kilka ciekawych informacji na temat możliwości Scali, które będziesz później wykorzystywał w wielu innych miejscach. Zatem nie przedłużając

Stała jest określana przy pomocy val, zmienna przy pomocy var. Mamy więc stałą: 

val stala: Int = 12 

I zmienną:

var zmienna: String = "Jestem sobie string"

Jak widać w Scali możemy połączyć definicje od razu z deklaracją, co jest bardzo wygodne, a nie każdy język na to pozwala.

Co jest również bardzo przyjemne w Scali to możliwość pominięcia typów danych w wielu miejscach, np. przy definicji stałych i zmiennych. Nie ma zatem przeszkód, żeby nasze wcześniejsze deklaracje zapisać np. tak:

val stala2 = 34
var zmienna2 = "Tez jestem stringiem"

Można się poczuć prawie jak w Pythonie. Tyle, że w Scali kompilator wyświetli komunikat błędu jak pomieszasz typy, a w Pythonie, cóż. Babol wyjdzie dopiero w testach albo na produkcji, jeśli pominiesz testy, albo przetestujesz za mało.

Jeśli chcesz, zapisać to w jednej linijce, możesz użyć średnika. czyli można to zapisać np tak:

val stala2 = 34; var zmienna2 = "Tez jestem stringiem"

Generalnie średnik można wstawiać na zakończenie każdej linii kodu, tylko ... po co?

Kolejnym bardzo ciekawym i istotnym elementem składni Scali jest przedrostek lazy.

Tak jest, Scala potrafi być leniwa i robi to doskonale. Do czego to? Przede wszystkim z lazy można korzystać wyłącznie w przypadku stałych. Wyobraź sobie, że masz algorytm, w którym w niektórych jego przejściach nie robisz prawie nic, a w innych kilkukrotnie używasz stałej której wyliczenie jest długotrwałe. Np. pobierasz dane z bazy. Możesz sobie stworzyć definicje:

lazy val leniwa = długie_obliczenia()

Teraz spokojnie używaj swojej stałej w dowolnym miejscu kodu. Co daje lazy? Przy napotkaniu deklaracji tej stałej, gdy maszyna javy zorientuje się, ze stała jest lazy, zapamięta że istnieje taka stała, ale nie wypełni jej danymi, czyli ta długotrwała operacja nie zostanie wykonana. Stała zostanie faktycznie wypełniona danymi dopiero gdy algorytm dojdzie do miejsca jej pierwszego użycia.

Przy każdym przejściu algorytmu, kiedy stała nie będzie fizycznie użyta, Twój program nie będzie marnował czasu na jej wyliczanie. Jaki z tego morał? Bądź leniwy, dbaj o środowisko, nie marnuj czasu procesora i pamiętaj o lazy. Bo każdy dobry programista jest leniwy.

Komentarze i podstawy ScalaDoc

To będzie krótki rozdział w którym zademonstruję Ci jakie są możliwości komentowania kodu w Scali.

Najprostszy komentarz do oczywiście komentarz jednolinijkowy, który uzyskujemy przy pomocy dwóch slashy

  // komentarz

Jeśli chcesz skomentować więcej linii kodu możesz to rozwiązać łącząc slash i gwiazdkę jak w przy kładzie

		 
/* Startuje komenatarz
i dalej mam komentarz
i dalej
i dalej
i teraz go kończę */>
  

Na koniec wspomnę jeszcze o możliwości tworzenia komentarzy dokumentujących. W Scali możesz do tego użyć składni nazwanej ScalaDocJeśli znasz składnie JavaDoc nic nie powinno Cię zaskoczyć), teraz tylko prosty przykład formatowania komentarza w ScalaDoc:

		 
** Początek komentarza
*
* będzie tu mozna używać specjalnych atrybutów 
* zaczynających się od @ np. @param
* które umożliwią przygotowanie ładnej dokumentacji
* tworzonego przez Ciebie kodu
*
* koniec komentarza */  

Procedury, metody i funkcje

Nazwę funkcja / metoda będę na tym tutorialu używał zamiennie. Jak dowiesz się później nie jest to do końca poprawne podejście. Ale po pierwsze jest po prostu wygodne, po drugie nawet Martin w swoich książkach nie jest do końca spójny w nazewnictwie.

Funkcję definiujemy przy pomocy słowa kluczowego def, po którym następuje nazwa funkcji, w nawiasach podajemy argumenty, a następnie po dwukropku typ wymaganego rezultatu, całość kończymy znakiem = i ciałem naszej funkcji.

def nazwa_funkcji(argumenty): TYP_REZULTATU = { ciało funkcji }

Rezultatem funkcji jest zawsze wynik ostatniej wykonanej w ramach tej funkcji operacji.

Przykładowo, poniższa funkcja zwróci w rezultacie 1:

def func1: Int = 1

Jak widzisz, w pewnych sytuacjach można pominąć nawiasy klamrowe. Czytałem kiedyś że w przypadku takich "jednolinijkowców" wpływa to nawet na szybkość wykonywania ich kodu, nie wiem jednak na ile jest to prawdziwa informacja.

A teraz funkcja, która zwróci 2:

def func2: Int = {
  val x = 1
  val y = 2
  y
}

Jeśli potrzebujesz opuścić funkcję wcześniej możesz do tego użyć komendy

return wartość,

zakończy ona działanie funkcji i zwróci określoną wartość. Jak łatwo się domyślić rezultatem poniższej funkcji będzie 1.

def func2: Int = {
  val x = 1
  return x
  val y = 2
  y
}

Czym jest procedura? Procedura ma nieco uproszczoną składnię w stosunku do funkcji, ale generalnie jest uznawana za konstrukcję przestarzałą i nie powinno się jej używać. Piszę o niej tylko dlatego, żeby taka składnia nie zaskoczyła Cię przy przeglądaniu czyjegoś kodu czy starszych książek.

Jak wygląda procedura?

def proc (argumenty) { ciało }

Jak widać w procedurze brakuje typu rezultatu oraz znaku =. Wynika to z faktu iż przyjmuje się, że procedura nie zwraca rezultatu. Nie jest to oczywiście do końca prawda, ponieważ w Scali każda operacja zwraca jakiś rezultat. W tym przypadku jest on typu Unit (w uproszczeniu odpowiednik Jawovego void)

Obecnie jest jednak zalecane stosowanie składni funkcji z jawnie podanym typem Unit jako rezultatem.

def function(argumenty): Unit = { ciało }

Tutaj mała, ale istotna uwag. Scala pozwala pominąć deklarowanie typu zwracanego przez funkcję (tak samo jak miało to miejsce przy stałych i zmiennych).

def func1 = 1

Jest to rozwiązanie które może łatwo spowodować powstanie trudno wykrywalnego błędu. Zalecane jest deklarowanie typu, jaki powinna zwracać dana funkcja mimo iż technicznie nie musimy tego robić.

Argumenty metod i funkcji

W tym oraz dwóch kolejnych rozdziałach zademonstruję Ci w jaki sposób można w Scali używać argumentów w funkcjach czy metodach.

Przede wszystkim zawsze konieczne jest podanie typu argumentu (typ można mocno zgeneralizować, ale o tym dowiesz się później), tutaj w przeciwieństwie do deklarowania stałych czy zmiennych Scala nie pozwala na użycie konwencji zapisu bez podania typu. 

Pozwala za to na sporo innych rzeczy.

Weźmy dla przykładu funkcję:

def example(v1: Int, v2: String = "nic", v3: String = "coś"): Unit = { () }

Jak myślisz na ile sposobów można wywołać tę funkcję? Większość zapewne odpowie, że na 3:

example(1);
example(2, "nie nic");
example(3, "nie nic", "i nie coś");

Scala pozwala jednak na dużo większą dowolność i elastyczność, ułatwiając jednocześnie samodokumentowanie kodu.

example(v3 = "jestem pierwszy", v2 = "jestem drugi", v1 = 4)
example(v2 = "jestem pierwszy", v3 = "jestem drugi", v1 = 5)
example(v3 = "jestem drugi", v1 = 6)

Podsumowując, argumenty można przekazywać w dowolnej i wygodnej dla nas kolejności, pod warunkiem iż użyjemy składni z nazwami tych argumentów.

Dlaczego jest to wygodne? Zapewne można by znaleźć sporo powodów, ale dla mnie najistotniejszym jest jest wspomniane wcześniej samodokumentowanie kodu. Zwłaszcza gdy pracujemy z typami Boolean.

def example2(switch: Boolean): Unit = { () }

Zamiast tworzyć stałe, w celu czytelnego użycia funkcji w kodzie (jak w poniższym przykładzie):

val SWITCH_ON = true
val SWITCH_OFF = false

example2(SWITCH_ON)

Możesz po prostu napisać:

example2(switch = true)

Z punktu widzenia czytelności kodu uzyskasz ten sam efekt, a pisania zdecydowanie mniej, nie trzeba też pamiętać nazewnictwa stałych.

Call by name, call by value, zagnieżdżanie

Znasz już standardową metodę przesyłania argumentu do funkcji / metody. W Scali jest on nazywany call-by-value. Czyli jako argument jest przekazywana określona wartość używana później w czasie wykonywania funkcji.

Nie jest to jedyna metoda. Druga to tzw. call-by-name. występują tutaj trzy różnice.

  1. Różnica w deklaracji def x(a: Int) vs def x(a: => Int)
  2. Różnica w wartości startowej. W przypadku call-by-value, wartość jest znana od początku. W przypadku call-by-name wartość zostanie wyliczona w momencie pierwszego użycia (coś jak lazy val)
  3. W przeciwieństwie jednak do lazy val, przy każdym kolejnym użyciu argumentu typu call-by-name w funkcji, za KAŻDYM razem jego wartość będzie wyliczana od nowa.

Czas na prosty przykład ilustrujący takie właśnie zachowanie Scali

def time(): Long = {
    println("Pobieram time()")
    System.nanoTime
}

def exec_by_value(t: Long): Long = {
    println("Wszedłem w exec - zwracam t")
    println("t = " + t)
    println("Ponownie zwracam t ...")
    t
}

def exec_by_name(t: => Long): Long = {
    println("Wszedłem w exec - zwracam t")
    println("t = " + t)
    println("Ponownie zwracam t ...")
    t
}

Nie pozostaje nam nic jak przetestować obie funkcje.

println(exec_by_value(time()))

I drugie podejście

println(exec_by_name(time()))

Jak było widać w pierwszym przypadku, czas był wyznaczony tylko raz. W przykładzie drugim, był wyznaczany przy każdym odwołaniu do argumentu t.

Na zakończenie tej lekcji jeszcze prosty przykład zagnieżdżania funkcji.

 def x(i: int): Boolean = {  
   
   def b(i: Int): Boolean = { 
     i > 100 
   }  
 
   b(i) 
 }

Jestem pewny, ze nie muszę Ci niczego tłumaczyć, chciałem tylko zademonstrować, że tego typu zagnieżdżenie jest jak najbardziej możliwe i całkowicie "legalne".

Argumenty powtarzalne

Nadszedł czas na przesłanie do funkcji argumentów o nieokreślonej ilości. 

Dwie uwagi na początek:

  1. Tylko jeden atrybut może być zadeklarowany w ten sposób
  2. Musi być to ostatni argument funkcji

Argument taki deklarujemy dodając do niego gwiazdkę.

def mul(x: Int*): Int = {
  x.product      
}

// możliwe wywołania

// 1
println(
  mul(10)
)

// 2 
println(
  mul(1,2,5)
)

// 3        
println(
  mul(
    Array(7, 3, 2, 10): _*
  )
)

Kilka istotnych uwag:

  1. Dlaczego używam zapisu Array(7, 3, 2, 10): _*)? Dlatego, że mamy przesłać do funkcji nie pojedynczy argument będący Arrayem, a wielu pojedynczych argumentów.

  2. Co to za element składni _*? Wymusza on na kompilatorze przesłanie każdego elementu (w tym przypadku Arraya), jako pojedynczego argumentu, zamiast Arraya jako całości

  3. Od wersji Scali 2.13 zapis taki jest przestarzały. Pozostawiłem do dla tego, żebyś trafiając na niego w starszym kodzie rozumiał o co chodzi.

Jak powinno się w takim razie teraz wywołać fundację mul? Przykład 1 i 2 pozostają bez zmian. Natomiast zamiast Arraya należy użyć Set.

Można to zrobić albo bezpośrednio, albo pośrednio konwertując Array na Set.

println(
  mul(
    Array(7, 3, 2, 10).toSeq: _*)
  )
)

println(
  mul(
    Seq(7, 3, 2, 10): _*)
  )
)

Metody bez nawiasów i kropek

Teraz co nieco o stylu pisania kodu. Scala w pewnych określonych okolicznościach umożliwia pominięcie kropek czy nawiasów, nie zawsze jednak jest to działanie zalecane przez twórców języka.

Ogólne zasady są przyjęte podobnie jak w Javie. Przy użyciu metody używamy kropkę bez spacji, argumenty, oddzielamy przecinkiem, każdy argument powinien być oddzielony od wcześniejszego spacją. Nie stosujemy spacji w sąsiedztwie nawiasów.

foo(42, bar)
target.foo(42, bar)
target.foo()

Arity-0 (funkcja bez argumentów)

Można wywoływać zarówno z nawiasami jak i bez.

runIt()
runIt

Tutaj bardzo ważna uwaga, zaleca się, aby wywołanie bez nawiasowe, stosować WYŁĄCZNIE dla funkcji które nie generują side effect (opowiem Ci o tym w jednym z późniejszych rozdziałów). 

W uproszczeniu chodzi o to, że funkcja nie zmienia otoczenia. Nie wyświetla niczego na ekranie, nie zapisuje niczego do bazy, nie zmienia zmiennych globalnych.

Rygorystyczne trzymanie się tego zalecenia, spowoduje, że kod będzie o wiele bardziej czytelny i jednoznaczny.

Krótko mówiąc można pominąć nawiasy przy np. napis.size, ale nie powinno się tego robić w println().

Notacja postfixowa

Scala umożliwia, aby metody które nie mają żadnych argumentów wywoływać bez kropek:

names.toList 

// to to samo co

names toList

Powinno się unikać drugiego (bezkropokowego) zapisu, ponieważ może on generować trudne do zrozumienia błędy na etapie kompilacji.

Arity-1 (Infix Notation) (funkcja z jednym argumentem)

// zapis rekomendowany
names.mkString(",")

// spotykany czasami, ale mocno kontrowersyjny
names mkString ","

// błędny - mamy sideeffect (dodanie elementu do listy)
javaList add item

Dodatkowe uwagi o formatowaniu kodu

// poprawnie
"mr" + " " + "X"
a + b

// niepoprawny
"mr"+" "+"X"
a+b
a.+(b)

Często spotykany przykład bezkropkowego zapisu:

a max b

Struktura pliku - package

Czym jest package i do czego służy?

Jest to metoda, dzięki której Scala pozwala na tworzenie przestrzeni nazw (namespace) w celu zmodularyzowania programu.

Package jest tworzony przez umieszczenie jego nazwy na początku pliku.

package users

class User

Nazwy pakietów można zagnieżdżać

package users {
  package administrators {
    class NormalUser
  }
  package normalusers {
    class NormalUser
  }
}

Przy nazewnictwie pakietów powinno przyjąć się następujące założenie (jeśli organizacja ma własną stronę WWW).

<top-level-domain>.<domain-name>.<project-name>

Nie jest to jednak reguła i np. Akka używa uproszczonego nazewnictwa np:

package akka.cluster

Stosowanie pakietów ma bezpośrednie odzwierciedlenie w strukturze katalogów projektu, ale o tym porozmawiamy sobie innym razem.

Struktura pliku - importy

Krótka, ale bardzo potrzebna lekcja na temat importowania modułów.

Jeśli chcemy zaimportować wszystko z modułu users:

import users._

Jeśli chcemy zaimportować wyłącznie klasę User

import users.User

Jeśli potrzebujesz trochę więcej, możesz użyć następującej składni:

import users.{User, UserPreferences}

Czasami może się zdarzyć, że wystąpi konflikt nazw. Można go łatwo rozwiązać, dokonując konwersji nazwy na etapie importu np.:

import users.{UserPreferences => UPrefs}

W ten sposób zaimportowaliśmy UserPreferences, ale przemianowaliśmy je na UPrefs i z takiej nazwy można teraz korzystać.

Warto pamiętać, że z importów można korzystać w dowolnym miejscu kodu, wcale nie trzeba robić tego tylko na początku pliku, oczywiście informacje zaimportowane w środku kodu będą miały określone ograniczenia jeśli chodzi o ich widoczność (do klasy, metody itp.).

Jeśli wystąpi jakiś konflikt nazw, można w sposób wymuszony przeprowadzić importu z korzenia projektu przez:

import _root_.users._

Warto też pamiętać, że moduły takie jak scala czy java.lang są importowane domyślnie i nie trzeba tego wymuszać.

Formatowanie stringów (string interpolation)

Od Scali w wersji 2.10 można formatować stringi przy pomocy tzw. string interpolation.

Zasady: przed stringiem (konkretnie przed cudzysłowem), określam, o jaką interpolacje nam chodzi przy pomocy: s, f albo raw np.: s"Wywal to do logu".

Stałe i zmienne, są reprezentowane jako $nazwa, wyrażanie jako ${wyrażenie}, natomiast do pól i metod odwołujemy się przez ${obj.metoda}

val txt1 = "Scala"

println(s"$txt1 jest moim ulubionym językiem programowania")

case class user(name: String, right: String)

val u = user("Daniel", "root")
println(s"Użytkownik ${u.name} ma uprawnienie ${u.right}.")

I na koniec proste działanie:

println(s"10 + 10 = ${10 + 10}")

Interpolacja z f różni się sposobem formatowania stringów. Można tutaj użyć dodatkowych parametrów znanych z javovego printf.

val itemPrice = 10.5
println(f"Item Price : $itemPrice%.2f")

Pozostała jeszcze interpolacja raw. W tym przypadku wszelkie kody sterujące jak np. znak końca linii czy tabulatury są wyświetlane, a nie wykonywane. Jest to doskonałe rozwiązanie gdy zależy nam np na zalogowaniu do pliku dokładnego wyglądu stringu wejściowego, np. jsona.

Warto wiedzieć, że Scala umożliwia również definiowanie własnych interpolacji, więcej na ten temat można przeczytać tutaj.

Testy - podstawy

Przypuszczam, że doskonale wiesz, czym są testy i ich tworzenie jest naturalnym elementem Twojej pracy. Jeśli tak właśnie jest, to wybacz mi, proszę, kilka następnych zdań, w których postaram się najszybciej, jak to potrafię wprowadzić w świat testów, tych, którzy ich nie robili.

W tym tutorialu skoncentrujemy się na dwóch podstawowych rodzajach testów.

  • Testach jednostkowych - które przeprowadzamy bardzo niskopoziomowo i przy ich pomocy wychwytujemy błędy w pojedynczych funkcjach czy procedurach.
  • Testach integracyjnych - przy pomocy których testujemy współpracę między modułami naszej aplikacji, interakcje z bazą danych itp.

Po co nam testy?

Po to, żeby nasz kod działał poprawnie 🙂 Oczywiście, wszystko zależy od jakości naszych testów i obszaru kodu, jaki pokryjemy tymi testami.

Wiem, że wielu programistów uważa testy za zło konieczne i często ich nie robi, albo robi bardzo po macoszemu.

Jest to bardzo krótkowzroczne podejście, które odbije się czkawką przy pierwszej refaktoryzacji kodu.

Gdy wprowadzamy zmiany w wielu miejscach kodu jednocześnie, możemy doprowadzić do wielu nieprzewidywanych zachowań naszego programu. Jeśli jednak wszystkie te miejsca mieliśmy pokryte przez testy, wykryjemy wszystkie (no większość), ewentualnych błędów na etapie testowania, a nie dopiero na produkcji u testera ostatecznego, czyli klienta.

W kolejnym rozdziale przedstawię najczęściej używane biblioteki do tworzenia testów w Scali.

Zapraszam, też na mojego VLOGa, do odcinka, w którym opowiadam, o tym, czy programista powinien pisać testy oraz do odcinka, w którym Ola Kunysz wspaniale opowiada o TDD.

ScalaTest, JUnit, ScalaMock, ScalaCheck, specs2

ScalaMock - to nie jest typowy framework do tworzenia testów, jest to framework wspierający w tworzeniu testów przy pomocy "mockingu".

Oczywiście można do tego celu używać Javovych bibliotek:

JMock, EasyMock i Mockito, ale wkrótce się przekonasz, iż pracując w Scali, bardzo często pomija się Javowe rozwiązania, bazując na natywnych stworzonych w Scali. Dlatego też coraz większa ilość programistów buntuje się słysząc, iż Scala to tylko bardziej zaawansowana Java. Jest to po prostu nieprawda. Scala wykształciła własny ecosystem frameworków i bibliotek.

JUnit, ScalaCheck, specs2

Jeśli masz ochotę używać któregoś z tych frameworków, ponieważ już go znasz, nie ma problemu. Sam jednak nie miałem z nimi dłuższej styczności, więc nie będę się na ich temat wypowiadał.

Do czego będę Cię namawiał to użycie ‌ScalaTest.

‌Dlaczego?

  • Ma doskonałą dokumentację
  • Posiada bardzo rozbudowaną możliwość dostosowania formatowania testów do Twoich przyzwyczajeń
  • Jeśli bardzo chcesz, pozwala Ci nawet odpalać napisane przy pomocy tej biblioteki testy w JUnit 
  • ankiecie jetbrains w 2019 roku aż 77% ankietowanych programistów Scali napisało, że używa właśnie tego frameworka, to znaczy, że uzyskanie pomocy w sieci, jest o wiele prostsze niż w przypadku innych rozwiązań.

Chcesz Nauczyć Się Scali?

Jestem tutaj, by Ci pomóc.