Każdy programista powinien mieć w swoim repertuarze chociaż jeden dynamiczny język programowania. W poście tym wyjaśniam czym jest dynamiczny język programowania oraz dlaczego współczesny programista jest kaleki bez znajomości takiego – w taki sam sposób, jak kaleki jest bez biegłej znajomości GITa . Tekst jest napisany w sposób nieznoszący sprzeciwu, więc proszę brać to pod uwagę – autor stara się ograniczyć ilość tekstu :)

Moje jeszcze dość niewielkie doświadczenie w Scali pochodzi głównie z aktualnego projektu pisanego w Play framework w Londynie. Na końcu opisuję, dlaczego po fascynacji Clojure podchodziłem do Scali jak do jeża i czego mi w tym języku brakuje.

Dynamiczny język programowania

Intuicyjna definicja języka dynamicznego: taki język, w którym za pomocą stosunkowo niewielkiego nakładu pracy, uzyskujemy dużą korzyść biznesowa i dobrej jakości kod.

Dostatecznie dobrym doprecyzowaniem może być wymienienie cech popularnych języków dynamicznych: domknięcia, funkcje wyższego rzędu, elastyczne podejście do typów, wsparcie dla programowania funkcyjnego, często scaffolding.

Dlaczego język dynamiczny?

Języków dynamicznych używa się w celu uniknięcia niepotrzebnej złożoności rozwiązania. W idealnym świecie, programy składałyby się tylko z logiki biznesowej, a więc niekonieczny byłby kod spinający (ang. glue code). W praktyce, celem jest ograniczenie narzutu kodu architektury i umożliwienie skupienia się na kodzie i złożoności problemu domeny.

Przykładowo, aby w Javie albo C# napisać program dodający 2 do 2, trzeba najpierw napisać całą klasę z metodą main zawierającą interesujący nas kod. W Scali wystarczy jedna instrukcja dodająca dwie liczby. Kompilator zajmuje się całą techniczną otoczką.

Ciekawszym przykładem jest programowanie funkcyjne w Scali. Dzięki korzystaniu z niemutowalnych obiektów i bogactwu konstrukcji z wykorzystaniem funkcji map, fold, reduce i innych, programy są prostsze i łatwiejsze do zrozumienia. Można w ten sposób uzyskać deklaratywny kod, który nie koncentruje się na sposobie dojścia do wyniku ale na samym wyniku.

Standardowe imperatywne podejście do problemu dodania do siebie liczb od 1 do 100 za wyjątkiem tych, które dzielą się przez 5:

int sum = 0;
for (int i = 0; i < 100; i++)
{
    if (i % 5 != 0)
      sum += i;
}

Odpowiednik w Scali:

 (1 until 100).filter(i=> i%5!=0).sum

Chyba oczywiste jest, który kod jest łatwiejszy do zrozumienia i koncentruje się na celu jaki chcemy osiągnąć a nie na sposobie.

Kilka najbardziej użytecznych konstrukcji w języku Scala

Opcje – dzięki opakowaniu typu w opcję można uniknąć wszechobecnych NullPointerExceptions. W przypadku, kiedy referencja nie przechowuje wartości, jej wartość wynosi None, a nie null. Często wykorzystuje się tę cechę w kolejnej bardzo przydatnej konstrukcji: pattern matching.

Pattern matching to potężne narzędzie do sterowania wykonywaniem kodu, obsługi różnych typów wyjątków, a także do unikania nieporęcznych konstrukcji if-else.

Prosty przykład z wykorzystaniem opcji i pattern matchingu:

 val a = someObject.doSomething() // zwraca Option[String]
a match {
	case Some(s) => println(“Zwrócono string:” + s) //1
	case None => println(“Nic nie zwrócono”) //2
}

W przypadku gdy wartość “a” zostanie dopasowana do opcji zawierającej cokolwiek, zostanie wykonany kod z linii 1, a jeśli “a” nie ma wartości, zostanie wykonana linia oznaczona dwójką. Warto zauważyć, że “a” nie jest zmienną. Nie można przypisać do niej już innej wartości w tym zasięgu.

Kolejny przykład pokazuje, jak wygodnie można obsługiwać wyjątki z wykorzystaniem tej konstrukcji.

 try {
	//nasz kod
} catch {
              case _: VirusDetectedException => JsonError("A virus was detected")
              case e @ (_: InvalidMimeTypeException | _: MatchNotFoundException) => JsonError("File format not recognized")
              case _ => JsonError("Storing attachment failed")
}

Pattern matching ma dużo więcej możliwości. Możliwe jest na przykład dopasowanie na podstawie typu obiektu, zawartości kolekcji, wartości pola w obiekcie, zgodności w wyrażeniem regularnym itp. Ciekawym zastosowaniem jest użycie go w celu rekurencyjnego analizowania struktur danych. Poniższy kod spłaszcza zagnieżdżone listy:

def flatten[A](list: List[A]): List[A] = list match {
   case Nil => Nil
   case (ls: List[A]) :: tail => flatten(ls) ::: flatten(tail)
   case h :: tail => List(h) ::: flatten(tail)
}

Niemutowalne kolekcje są praktycznie niezbędne do programowania funkcyjnego. Pozwalają zachować czystość kodu i nie wprowadzają niepotrzebnych skutków ubocznych. Poniższy kod nie dodaje do istniejącej mapy “m” nowej pary klucz->wartość, tylko tworzy nowy obiekt mapy zawierający obie pary.

val m = Map("a"->"aa")
//m: scala.collection.immutable.Map[String,String] = Map(a -> aa)

val m2 = m + ("b"->"bb")
//m2: scala.collection.immutable.Map[String,String] = Map(a -> aa, b -> bb)

Funkcje charakterystyczne dla języków funkcyjnych takie jak map i reduce są również dostępne w Scali. Sprawdzają się bardzo dobrze podczas pracy przy obróbce danych.

List("a","b","c","d").reduce((acc, v) => acc + ", " + v)
//res157: String = a, b, c, d

Powyższy przykład tworzy ładną reprezentację elementów listy, wypisując je po przecinku. Odpowiednik z wykorzystaniem konstrukcji “for” wyglądałby dosyć skomplikowanie, gdybyśmy chcieli uniknąć niepotrzebnego przecinka po ostatnim elemencie. Na marginesie: jest lepszy sposób na wydrukowanie stringów z listy, a mianowicie funkcja List.mkString.

W Scali jest dużo lukru, czyli konstrukcji mających upiększyć kod. Wśród nich są argumenty nazwane, arg. domyślne (default), arg. domniemane (implicit), rozwijanie funkcji (currying) i upodobnienie ostatniej listy argumentów bloku kodu.

Może nas przez to czekać kilka niespodzianek. Pamiętam jak bardzo się zdziwiłem, że listę argumentów funkcji można podać także w nawiasach klamrowych, a nie tylko w okrągłych. Przykład poniżej:

def filter(p: ((A, B)) ⇒ Boolean): Map[A, B]
(1 until 10).filter{i=> 
	//kilka 
	//linijek
	//kodu
}

Klasą samą w sobie są dość kontrowersyjne wartości i argumenty implicit, których wartość jest pobierana z aktualnego kontekstu. Oczywiście zasady wstrzykiwania implicitów są ściśle określone jednak to tylko nieznacznie ułatwia zadanie analizy kodu :)

Dlaczego nie Scala?

Statyczne typowanie
Jak można było zauważyć w tym poście, w ogóle nie skupiałem się na obiektowej naturze Scali i jej statycznemu typowaniu. Niestety typowanie wiele razy wchodziło mi w drogę i zamiast skupiać się na rozwiązywaniu problemu, walczyłem z obchodzeniem ograniczeń nakładanych przez typy. Dla niektórych może być to zaleta “bezpiecznego” języka, ale ja skłaniam się ku opinii, że to niepotrzebna perwersja :) Język typowania w Scali jest dość skomplikowany i sam w sobie mógłby stanowić osobny byt. Niestety przez to Scala jest mniej dynamiczna, niż by się chciało.

Kompilacja
Bardzo fajną zaletą Play framework jest możliwość dokonania zmian w kodzie i zobaczenia zmian przy odświeżeniu strony – bez konieczności restartu aplikacji. Niestety kompilator Scali jest odczuwalnie za wolny i cały efekt bardzo psuje.

REPL
Programując w Clojure, mamy do dyspozycji potężną konsolę (REPL), w której można programować interaktywnie. Oznacza to, że można wpiąć się w kontekst naszego kodu, podmieniać definicje funkcji i symboli, monitorować wartości – jednym słowem wygodnie i dowolnie żonglować kodem. Scala również posiada konsolę, ale każdorazowa zmiana w kodzie źródłowym wymaga wyjścia z konsoli i ponownego wejścia do niej, aby zmiany doszły do skutku. Powoduje to, że użyteczność tego narzędzia jest znikoma.

Kod jako dane
Napotkałem na problem znalezienia różnic między dwoma obiektami. Scala nie traktuje kodu klas jako zwykłych danych. Aby uniknąć chodzenia po polu minowym, używając refleksji, musiałem skonwertować obiekty do JSONa i pracować na takiej strukturze. Dosyć gorzkie uczucie po doświadczeniach z Clojure.

Cukier a prostota
Mnogość konstrukcji i skomplikowanie gramatyki języka wpływa na szybkość uczenia się go. Co gorsza, taka komplikacja utrudnia także czytanie kodu. Uważam, że języki dynamiczne powinny wystrzegać się nadmiernej komplikacji składni, ponieważ odwraca ona uwagę od prawdziwych problemów.

Podsumowanie
Pomimo paru zastrzeżeń, ogólne wrażenie na temat Scali mam pozytywne. Przy przesiadce z Javy czy C# można docenić możliwość wygodnego programowania funkcyjnego. Zbyt często jednak podczas dnia pracy nachodzi mnie myśl, że jest jeszcze lepsze i bardziej efektywne narzędzie – Clojure.

Zapraszam na mojego bloga, na którym możecie poczytać trochę więcej o Clojure i językach JVM: http://jvmsoup.com