Archiwa tagu: programowanie funkcyjne

Zawrót głowy w Scali: class, trait, object, instancja

Fajne rzeczy w Scali: klasy przypadkówKiedy jako programista Javy po raz pierwszy ujrzałam kod napisany w Scali, miałam spore problemy ze zrozumieniem, czym różnią się od siebie poszczególne byty:

  • class (klasa)
  • case class (klasa przypadków)
  • trait (cecha)
  • object (obiekt)
  • instancje klas (bo „obiekt” w Scali to wcale nie jest instancja!).

Poniżej ściąga dla zainteresowanych i dla przyszłej mnie 🙂

Klasa

Klasa (class) jak w przypadku większości obiektowych języków programowania, to szablon, na podstawie którego można tworzyć ukonkretnione instancje. Zawiera pola i metody. Ponieważ Scala jest językiem funkcyjnym, dobrym zwyczajem jest tworzenie klas niemodyfikowalnych.

Klasa przypadków

O klasach przypadków (case class) więcej napisałam tutaj: Fajne rzeczy w Scali: klasy przypadków. Klasy przypadków to specjalne klasy, których instancje można porównywać przez wartość i dopasowywać do wzorców w ramach instrukcji match.

Cecha

Cechy (trait) są podobne do klas i umożliwiają wielokrotne dziedziczenie (które ostatecznie i tak jest linearyzowane przez kompilator, ale nie o tym teraz). Różne byty (klasy, obiekty, cechy) w Scali mogą rozszerzać jedną tylko klasę, ale za to wiele cech.

Przy dziedziczeniu z kilku cech trzeba użyć słowa kluczowego with:

Jeśli wśród „rodziców” znajduje się jakaś klasa, musi ona zostać wymieniona pierwsza, zaraz po słowie extends.

Nie można tworzyć instancji cech.

Cechy można dodawać do klas na etapie ich definicji, ale także do instancji na etapie ich tworzenia. Cechę można dorzucić do właściwie każdej klasy – chyba że ograniczymy listę dozwolonych typów przy użyciu słowa kluczowego self (co ma sens np. jeśli kod którejś z metod odwołuje się do pola istniejącego tylko w danej hierarchii klas).

Obiekt

Obiekt (object) w Scali to klasa, która może miec maksymalnie jedną instancję – a więc, innymi słowy, singleton. Instancja ta zostanie utworzona w momencie pierwszego dostępu do niej przez JVM. Obiekty umożliwiają tworzenie odpowiedników metod i pól statycznych (globalnych), tj. atrybutów niepowiązanych z konkretną instancją klasy.

Obiekt może dziedziczyć z jakiejś klasy albo cechy.

Często stosowanym wzorcem w Scali są obiekty towarzyszące. Obiekt towarzyszący ma taką samą nazwę jak klasa, jest definiowany w tym samym pliku i ma (z wzajemnością) dostęp do jej prywatnych pól i metod. Obiekty towarzyszące mogą pełnić funkcję fabryki instancji danej klasy.

Kolejne zastosowanie obiektów to aplikacje w linii poleceń. Wewnątrz obiektu można zdefiniować metodę main, która zostanie uruchomiona po wywołaniu kodu z linii poleceń.

Instancja

Instancja to instancja 🙂 czyli konkretny jeden byt stworzony w oparciu o wzór opisany w ramach klasy.

Przyznam, że w mojej własnej głowie nietypowe użycie słowa object narobiło sporo zamieszania. Podejrzewam, że w miarę poznawania języka zrozumiem, dlaczego tak się stało i przestanie mnie to zaskakiwać.

Gdzie znaleźć więcej szczegółów?

Oto kilka źródeł dogłębniej tłumaczących omawiane to różnice:

Fajne rzeczy w Scali: klasy przypadków

Skończyłam pierwszy z pięciu kursów programowania w języku Scala w pięciokursowej specjalizacji na Courserze, prowadzonej przez twórcę tego języka.

Kolejna ciekawa cecha języka, jaką poznałam, to klasy przypadków (ang. case classes) i dopasowywanie wzorców (ang. pattern matching).

Instrukcje wielokrotnego wyboru i wzorce w innych językach

W wielu językach programowania istnieje jakaś forma instrukcji switch. W Javie pozwala  ona dokonywać wyboru dla zmiennej typu prostego oraz, nie od początku, zmiennej typu String. Na przykład:

Dopasowywanie wzorców i instrukcja match w Scali

Niektóre języki, w tym Scala, idą o krok dalej. Instrukcji wielokrotnego wyboru można w nich używać nie tylko na danych typów prostych. Co więcej, dopasoowywanie wzorców pozwala nam opisać przypadki w sposób niepełny – np. z zastosowaniem symboli wieloznacznych (ang. wildcards).

Poniżej przykład ze Scali, w którym przetwarzane są wierzchołki drzewa binarnego – osobno wierzchołki wewnętrzne (Fork), osobno liście (Leaf) drzewa.

W zależności od typu wierzchołka (Fork lub Leaf) oraz od wartości jednego z pól w przypadku klasy Leaf, w odmienny sposób zwrócona zostanie waga wierzchołka.

Nieistotne parametry zostały zastąpione symbolem _, który dopasuje się do wszystkiego.  Istotnym parametrom przypisujemy nazwę, dzięki czemu będzie można się do nich odwołać. Można też użyć ukonkretnionych wartości, żeby zdefiniować konkretne przypadki.

Po prawej stronie case nie ma żadnego przypisania ani instrukcji, ponieważ match jest wyrażeniem – zwróci jako wartość prawą stronę operatora =>.

Test  powyższej funkcji (przechodzący, daję słowo):

Klasy przypadków i ich cechy

Brakuje jeszcze definicji klas Leaf oraz Fork. Jak łatwo się domyślić, żeby używać ich w ten sposób, klasy te muszą zostać zdefiniowane jako klasy przypadków.

Ich definicja wygląda tak:

Klasy przypadków:

  • definiuje się z użyciem słowa kluczowego case,
  • powinny być niemodyfikowalne,
  • są porównywane przez podobieństwo strukturalne i wartości, a nie przez referencję,
  • wartości przekazane w konstruktorze są publicznie dostępne.

FAQ

Poniżej kilka szybkich pytań i odpowiedzi:

Q1: Co się stanie, jeśli nie przewidzisz wszystkich wzorców i na etapie wywołania okaże się, że zmienna nie pasuje do żadnego wzorca?
A1:  Otrzymasz scala.MatchError.

Q2: Jak utworzyć odpowiednik javowego default?
A2: Tak:

Q3: Czy to działa także dla typów prostych.
A3: Tak. Co więcej, możliwa jest nawet taka operacja:

PS.

Bardzo to wszystko wygodne i czytelne.

O, podstawowa dokumentacja Scali została przetłumaczona na język polski!

Fajne rzeczy w Scali: zwracanie funkcji

Kontynuuję kurs programowania w języku Scala, o którym wspominałam w poprzednim wpisie.  Zgodnie z obietnicą, pokazuję kolejną fajną rzecz, której się nauczyłam.

Funkcje, które zwracają funkcje!

W Scali funkcje są tzw. obywatelami pierwszej kategorii (ang. first class citizens).  Inaczej, funkcje są pierwszoklasowe. Oznacza to, że można z nimi robić wszystko to, co można robić z „normalnymi” typami danych, to jest: przekazywać jako argumenty, przypisywać do zmiennych, zwracać z funkcji, tworzyć je na bieżąco.

W szczególności, jedna funkcja może zwrócić inną. Poniżej przykład.

Na początku tworzymy alias o nazwie Set. Reprezentuje on typ funkcyjny: wszystkie funkcje pobierające parametr typu Int i zwracające wartość typu Boolean. W ten sposób chcemy, na potrzeby przykładu, reprezentować zbiory: jako funkcję, która dla każdej podanej liczby całkowitej powie nam, czy liczba ta należy do tego zbioru.

Jeśli nasz zbiór ma być zbiorem jednoelementowym (ang. singleton), to w zależności od tego, jaki to element, musielibyśmy utworzyć (nieskończenie) wiele funkcji sprawdzających, przynależność do zbioru – dla każdego możliwego elementu zbioru jednostkowego. Osobno dla zbioru zawierającego tylko liczbę 1, osobno dla liczby 2 itp. Dzięki możliwości zwracania funkcji, mamy tylko jedną funkcję, która tworzy i zwraca funkcję badającą przynależność do zbioru jednostkowego zawierającego wybrany element – przekazany jako parametr do funkcji singletonSet.

Jak to działa w praktyce?

Na początku testu tworzymy zbiór jednoelementowy zawierający liczbę całkowitą 4. Nasz zbiór – czyli funkcję pobierającą argument Int, zwróconą z funkcji singletonSet – przypisujemy do zmiennej (hm, stałej właściwie) single.

Następnie, wywołując funkcję przypisaną do zmiennej single, sprawdzamy, czy do zbioru {4} należą odpowiednio liczby 4 oraz 1. Zgodnie z oczekiwaniami, dla 4 otrzymujemy wartość true, dla 1 wartość false.

Uczę się Scali!

Zapisałam się na kurs programowania w języku Scala. Prowadzi go na Courserze twórca tego języka, Martin Odersky.

Postanowiłam po każdym tygodniu kursu dzielić się jakimś spostrzeżeniem albo cechą języka.

Co spodobało mi się w pierwszym tygodniu? Możliwość definiowania funkcji zagnieżdżonych, czyli funkcji w funkcjach. Poniżej przedstawiam definicję funkcji rekurencyjnej spradzającej parzystość nawiasów. Pomocnicza funkcja z dodatkowym parametrem jest zdefiniowana wewnątrz niej.

Co to daje? Przede wszystkim, funkcje pomocnicze przeznaczone do jednokrotnego użycia nie zaśmiecają przestrzeni nazw. Oczywiście w Javie zdefiniowałabym taką funkcję jako prywatną, więc do „zaśmiecenia” doszłoby tylko w ramach jednej klasy – ale i to potrafi uprzykrzyć kodowanie, np. kiedy IDE podpowiada mi wersje metody zamiast jednej.

Scala nie jest jedynym językiem pozwalającym na stosowanie zagnieżdżonych funkcji. Pełna lista jest dostępna w Wikipedii, w artykule o funkcjach zagnieżdżonych (opisane są tam też „obejścia” tego problemu w językach bez bezpośredniego wsparcia dla takich funkcji).

Inna rzecz, która przykuła mogą uwagę na kursie, to akcent prowadzącego. Nie mogłam się oprzeć i sprawdziłam, skąd pochodzi. Okazało się, że to Niemiec, ale pracujący we francuskojęzycznej części Szwajcarii.

Swoją droga, bardzo podobają mi się zadania domowe. Wymagają chwili pomyślunku, dobrze ilustrują treść wykładów – a przy tym nie zajmują bardzo dużo czasu.

Zachęcam do przyłączenia się do mnie na kursie!