Blog (30)
Komentarze (5.6k)
Recenzje (0)

Scala — pierwsze kroki cz.5

@mikolaj_sScala — pierwsze kroki cz.506.12.2014 14:51

Język Scala oprócz tego, że jest językiem o funkcyjnym paradygmacie programowania, to również jest w pełni obiektowa. Istnienie metod i klas statycznych w Javie powoduje, że puryści językowi twierdzą, że nie jest ona w prawdziwym obiektowym językiem programowania. Scala rozwiązała ten problem wprowadzając obiekty, które są czymś zbliżonym do wzorca singletonu. Każda klasa może posiadać odpowiadający jej obiekt o identycznej nazwie co klasa, posiadający tylko jedną wersję powstającą przy uruchamianiu programu. Innym problemem, który udało się rozwiązać jest dziedziczenie wielobazowe, dostępne w Javie tylko w szczątkowej formie interfejsów. W Scali mamy Traits czyli instancje podobne do klas, ale różniące się tym od nich, że nie posiadają konstruktora z parametrem. Klasa może dziedziczyć po jednej zwykłej klasie i równocześnie po wielu Traits. Definiowanie klas ma również pewne drobne ułatwienia jak chociażby fakt, że ciało klasy jest równocześnie jego konstruktorem.

Klasy

Przykładowa definicja klasy:


 class Licznik {
      println("Konstruktor")
      private var suma = 0
      def +=(x: Int) = suma += x 
      def -=(x: Int) = suma -= x
      def ? = suma.toString
   }

Kod napisany w nawiasach klamrowych, jeśli nie jest definicją metody, należy do konstruktora, co możemy sprawdzić tworząc instancję klasy:

val l = new Licznik

Jak widać konstrukcja instancji wymaga słowa kluczowego new, a nawiasy po nazwie klasy są opcjonalne. Słowo Konstruktor zostanie wydrukowane na standardowe wyjście zaraz po utworzeniu instancji klasy Licznik. Teraz możemy wywoływać metody klasy:


 l.+=(234)
l.?
l -= 12
I ?

Jak widać możemy wywoływać metody klasycznie z kropkami i nawiasami lub bez nich oddzielając obiekt, nazwę metody i parametr spacją. (Dla metod z kilkoma parametrami muszą być nawiasy). W przypadku metody oznaczonej znakiem zapytania nie można wywołać jej z nawiasami, ponieważ nie zostały one użyte w czasie jej definicji. Definicje klas zostały tutaj napisane w skróconej formie, której używać możemy tylko gdy definicja metody ma tylko jedną linijkę. W przeciwnym przypadku niezbędne są jeszcze nawiasy klamrowe. Podobnie jak w Javie mamy słowa kluczowe private i protected, zaś wszystko co nie jest oznaczone jednym z tych słów uznane jest za publiczne. Aby dodać dodatkowy konstruktor piszemy definicję nowej metody klasy o nazwie this.

Dziedziczenie

[code=Scala]class LicznikNazwany(nazwa:String) extends Licznik { override def ? = nazwa + ": " + super.?.toString } [/code]

Tak jak w Javie możemy dziedziczyć tylko po jednej klasie. Robimy to używając słowa kluczowego extends. Tym razem nasz konstruktor przyjmuje parametr, więc musimy utworzyć instancję tej klasy dodając jej nazwę:

[code=Scala]val ln = new LicznikNazwany("Licznik1")[/code]

Próbujemy jak działa klasa:

[code=Scala]ln += 34 ln ? [/code]

Po przetestowaniu kodu zobaczymy, że różnica w porównaniu do klasy bazowej polega na dodawaniu do każdego wydruku nazwy licznika. Zauważmy, że w klasie dziecka przy nadpisywaniu metody ? użyto słowa kluczowego override. Jest ono obowiązkowe w takiej sytuacji. Słowo super pozwala upewnić się, że dana metoda jest wywoływana w klasie rodzica.

Hierarchia wbudowanych klas
Hierarchia wbudowanych klas

Obiekty

Obiekt jest czymś podobnym do singletonu. Ma identyczną nazwę z klasą i musi być zdefiniowany w tym samym pliku co dana klasa. Ma on dostęp do wszystkich składowych pól prywatnych klasy. Sam jest dostępny tak jak w Javie obiekt statyczny.

Utwórzmy obiekt towarzyszący naszej klasie:

[code=Scala]object LicznikNazwany { var licznik = 0; def apply() = { licznik += 1 new LicznikNazwany("Licznik: " + licznik.toString) } }[/code]

Możemy dopisać na początku deklaracji klasy LicznikNazwany słowo private, przez co nie da się utworzyć samodzielnie instancji tej klasy. Instancje tej klasy będziemy otrzymywać z naszego stowarzyszonego obiektu, dzięki metodzie apply. Metoda ta jest sposobem na przeładowanie operatora (). Dzięki temu możemy utworzyć instancje klasy LicznikNazwany jako:

[code=Scala]val licz = LicznikNazwany() [/code]

Nie jest to wywołanie konstruktora, tylko wywołanie na obiekcie LicznikNazwany metody apply. W taki sposób, jak niektórzy zapewne już zauważyli, utworzyliśmy prostą wersję wzorca projektowego fabryka.

Traits

Są one podobne do klas. Jednak nie mogą posiadać konstruktora. Możemy dziedziczyć po wielu traits bez ograniczeń (używając wiele razy słowa with). Przykładowe wykorzystanie:

[code=Scala]trait Filozof { def filozofia() { println("Używam RAM, więc jestem") } } abstract class Zwierze { def glos() } class Pies extends Zwierze with Filozof { def glos() {println("Hau hau!")} } val pies = new Pies pies.glos pies.filozofia [/code]

Traits dają nieco większe możliwości niż interfejsy w Javie, ponieważ mogą dostarczać już gotowy kod, tak jak w tym przypadku Filozof dostarcza metodę filozofia. Interfejsy Javy są w Scali traktowane jako traits z abstrakcyjnymi definicjami metod. W definicji metody glos() klasy Pies nie musimy używać słowa override, ponieważ definicja tej metody w klasie rodzica jest abstrakcyjna. Abstrakcyjna klasa może mieć również nieabstrakcyjne definicje metod.

Możliwe jest dodawanie traits nie w definicji klasy, tylko przy tworzeniu instancji klasy:

[code=Scala]class Kot extends Zwierze { def glos() {println("Miau!")} } val kot = new Kot kot.glos val kotFilozof = new Kot with Filozof kotFilozof.filozofia [/code]

Zatem możemy wytworzyć obiekty o różnym zachowaniu. Daje to dużo możliwości, ale warto jednak stosować umiar.

Przykład

Kod do tej pory przedstawiony jest bardzo prosty i nie oddaje w jaki sposób należy korzystać z obiektowości w języku funkcyjnym. Znacznie lepiej oddaje to przykład kodu jaki możemy znaleźć w "Programming in Scala", której współautorem jest twórca Scali Odersky:

[code=Scala]class Ulamek(licz: Int, mian: Int) { require( mian != 0) private val dziel = NWD(licz.abs, mian.abs) val li = licz / dziel val mi = mian / dziel def this(licz: Int) = this(licz, 1) def +(ulam: Ulamek): Ulamek = new Ulamek( li * ulam.mi + ulam.li * li, mi * ulam.mi ) def +(i: Int): Ulamek = new Ulamek(licz + i * mi, mi) def -(ulam: Ulamek): Ulamek = new Ulamek( li * ulam.mi - ulam.li * li, mi * ulam.mi ) def -(i: Int): Ulamek = new Ulamek(licz - i * mi, mi) def *(ulam: Ulamek): Ulamek = new Ulamek(li * ulam.li, mi * ulam.mi) def *(i: Int): Ulamek = new Ulamek(li * i, mi) def /(ulam: Ulamek): Ulamek = new Ulamek(li * ulam.mi, mi * ulam.li) def /(i: Int): Ulamek = new Ulamek(li, mi * i) override def toString = li +"/"+ mi def NWD(a: Int, b: Int): Int = if(b == 0) a else NWD(b, a % b) }[/code]

Kod ten realizuje ułamek zwykły (może być niewłaściwy) o całkowitym liczniku i mianowniku. Typowym zachowaniem dla stylu funkcyjnego jest brak metod w klasie, które mutowałyby samą siebie. Pola klasy są niezmienne (oznaczone jako val). Zdefiniowane są jako publiczne, ale dzięki niemutowalności nie ma potrzeby pisania geterów i seterów. Każda operacja na ułamkach powoduje powstanie nowej wartości. Taki kod dobrze się również sprawdza w przypadku programowania wielowątkowego, nie trzeba się martwić o synchronizację, przekazujemy między wątkami gotową instancję klasy. Polecenie require sprawdza warunek rzucając wyjątkiem w razie zerowego mianownika. Innym ciekawym udogodnieniem z jakim mamy do czynienia jest tail recursion (rekurancja ogonkowa) i zdolność kompilatora do zamiany jej na iterację. Metoda NWD obliczająca największy wspólny dzielnik jest napisana jako rekurencja, ale tak, że funkcja po zakończeniu nie musi wracać do poprzedniego wywołania w celu obliczenia wartości. Taką rekurencję kompilator Scali automatycznie zamienia w kodzie bajtowym na zwykłą iterację, dzięki czemu jest ona wydajna i nie zajmuje dużo miejsca w pamięci. Korzystanie z klasy Ulamek może wyglądać następująco:

[code=Scala]val x = new Ulamek(2, 3) val y = x * x + 1 val z = y / 4 [/code]

Możemy dodać do niego liczbę typu Int, jednak problemem może być fakt, że nie możemy wykonać np. operacji:

[code=Scala] val u = 2 + x [/code]

Problem ten można rozwiązać używając implicit conversion:

[code=Scala]implicit def intNaUlam(x: Int) = new Ulamek(x)[/code]

Jeśli spróbujemy teraz dodać do liczby typu Int ułamek, to kompilator zauważy, że nie ma takiej metody w Int, która brałaby jako argument typ Ulamek. Poszuka więc czy w zasięgu jest zdefiniowana metoda implicit, zamieniająca typ Int na Ulamek. Jeśli tak to wykona tę operację. Jest to bardzo wygodne narzędzie do tworzenia wszelkich DSLi, jednak trzeba uważać, żeby nie nadużywać go, ponieważ osobie korzystającej z naszego kodu może nieźle namieszać.

Podumowanie

Tak jak wspominałem jest to ostatni z zaplanowanych przeze mnie odcinków kursu. Oczywiście nie wyczerpuje to w żadnym stopniu tematu, a jedynie jest lekkim muśnięciem, pokazującym możliwości języka i jego cechy. Mam nadzieję, że przybliżył nieco tematykę osobom, które nie miały jeszcze z nim styczności, a może nawet zachęci niektórych z was do dalszego pogłębiania wiedzy. (Do czego jest mnóstwo książek w tym darmowa pierwsza edycja Programming in Scala. Są dwie po polsku. Można też znaleźć również mnóstwo kursów i tutoriali.) Nie jest to prosta droga bo faktycznie składnia łącząca programowanie funkcyjne i obiektowe wymaga nieco więcej wysiłku niż większość innych języków programowania. Fakt ten został już zauważony przez twórców Scali, wiele dużych firm używających Scali również zwraca na to uwagę. Dlatego jednym z celów zespołu pracującego nad następnymi wersjami ma być właśnie przyjrzenie się składni języka i odrzucenie rzeczy zbędnych, ale w taki sposób, aby nie stracić na ekspresyjności. Zmiany te jednak pojawią się dopiero za jakiś czas, ponieważ w tej chwili trwają prace nad wykorzystaniem nowych cech funkcyjnych JVM w wersji 8, pozwalających uprościć kompilację kodu i przyspieszyć niektóre z bibliotek Scali. Wydaje mi się, że język ten nie zdobędzie nigdy takiej popularności jak inne języki głównego nurtu. Barierą jest brak wystarczającej ilości programistów, którzy opanowali język i jego narzędzia na odpowiednim poziomie. Można jednak zauważyć, że są dziedziny, gdzie zdobywa on silną pozycję. Jest to głównie Big Data i podobne dziedziny, gdzie przetwarza się dużo danych w wielu wątkach, wykorzystując aktorów. Nie oznacza to, że tylko do tego się nadaje, Osoba (czego jestem sam przykładem), która zainwestuje swój czas w poznanie Scali, doceni szybkość jaką daje pisanie w tym języku aplikacji webowych po stronie serwera w takich frameworkach jak Lift, Spray.io czy Play.

Szanowna Użytkowniczko! Szanowny Użytkowniku!
×
Aby dalej móc dostarczać coraz lepsze materiały redakcyjne i udostępniać coraz lepsze usługi, potrzebujemy zgody na dopasowanie treści marketingowych do Twojego zachowania. Twoje dane są u nas bezpieczne, a zgodę możesz wycofać w każdej chwili na podstronie polityka prywatności.

Kliknij "PRZECHODZĘ DO SERWISU" lub na symbol "X" w górnym rogu tej planszy, jeżeli zgadzasz się na przetwarzanie przez Wirtualną Polskę i naszych Zaufanych Partnerów Twoich danych osobowych, zbieranych w ramach korzystania przez Ciebie z usług, portali i serwisów internetowych Wirtualnej Polski (w tym danych zapisywanych w plikach cookies) w celach marketingowych realizowanych na zlecenie naszych Zaufanych Partnerów. Jeśli nie zgadzasz się na przetwarzanie Twoich danych osobowych skorzystaj z ustawień w polityce prywatności. Zgoda jest dobrowolna i możesz ją w dowolnym momencie wycofać zmieniając ustawienia w polityce prywatności (w której znajdziesz odpowiedzi na wszystkie pytania związane z przetwarzaniem Twoich danych osobowych).

Od 25 maja 2018 roku obowiązuje Rozporządzenie Parlamentu Europejskiego i Rady (UE) 2016/679 (określane jako "RODO"). W związku z tym chcielibyśmy poinformować o przetwarzaniu Twoich danych oraz zasadach, na jakich odbywa się to po dniu 25 maja 2018 roku.

Kto będzie administratorem Twoich danych?

Administratorami Twoich danych będzie Wirtualna Polska Media Spółka Akcyjna z siedzibą w Warszawie, oraz pozostałe spółki z grupy Wirtualna Polska, jak również nasi Zaufani Partnerzy, z którymi stale współpracujemy. Szczegółowe informacje dotyczące administratorów znajdują się w polityce prywatności.

O jakich danych mówimy?

Chodzi o dane osobowe, które są zbierane w ramach korzystania przez Ciebie z naszych usług, portali i serwisów internetowych udostępnianych przez Wirtualną Polskę, w tym zapisywanych w plikach cookies, które są instalowane na naszych stronach przez Wirtualną Polskę oraz naszych Zaufanych Partnerów.

Dlaczego chcemy przetwarzać Twoje dane?

Przetwarzamy je dostarczać coraz lepsze materiały redakcyjne, dopasować ich tematykę do Twoich zainteresowań, tworzyć portale i serwisy internetowe, z których będziesz korzystać z przyjemnością, zapewniać większe bezpieczeństwo usług, udoskonalać nasze usługi i maksymalnie dopasować je do Twoich zainteresowań, pokazywać reklamy dopasowane do Twoich potrzeb. Szczegółowe informacje dotyczące celów przetwarzania Twoich danych znajdują się w polityce prywatności.

Komu możemy przekazać dane?

Twoje dane możemy przekazywać podmiotom przetwarzającym je na nasze zlecenie oraz podmiotom uprawnionym do uzyskania danych na podstawie obowiązującego prawa – oczywiście tylko, gdy wystąpią z żądaniem w oparciu o stosowną podstawę prawną.

Jakie masz prawa w stosunku do Twoich danych?

Masz prawo żądania dostępu, sprostowania, usunięcia lub ograniczenia przetwarzania danych. Możesz wycofać zgodę na przetwarzanie, zgłosić sprzeciw oraz skorzystać z innych praw wymienionych szczegółowo w polityce prywatności.

Jakie są podstawy prawne przetwarzania Twoich danych?

Podstawą prawną przetwarzania Twoich danych w celu świadczenia usług jest niezbędność do wykonania umów o ich świadczenie (tymi umowami są zazwyczaj regulaminy). Podstawą prawną przetwarzania danych w celu pomiarów statystycznych i marketingu własnego administratorów jest tzw. uzasadniony interes administratora. Przetwarzanie Twoich danych w celach marketingowych realizowanych przez Wirtualną Polskę na zlecenie Zaufanych Partnerów i bezpośrednio przez Zaufanych Partnerów będzie odbywać się na podstawie Twojej dobrowolnej zgody.