Kiedy zapytano Jamesa Goslinga (twórcę Javy) o to, który z języków programowania współpracujących z JVM (wirtualną maszyną Javy) by użył teraz, pomijając samą Javę, odpowiedź była zaskakująco szybka i bardzo jasna – Scala.
Nazwa “Scala” pochodzi od “Scalable language” (język skalowalny). Język ten nadaje się równie dobrze do krótkich, zwartych skryptów jak i do tworzenia wydajnych, ogromnych, bezpiecznych systemów sieciowych. W swych założeniach Scala nawiązuje do minimalizmu składni Lispa i Smalltalka (większość rzeczy oparta jest na bibliotekach a nie na składni) dzięki czemu język ten praktycznie nie ma ograniczeń rozwoju i doskonale się skaluje (w miarę potrzeb można tworzyć nowe typy i całe nowe struktury wyglądające jak nowa składnia języka).
Scala jest językiem kompilowanym do bytecodu JVM dzięki czemu potrafi się integrować w sposób praktycznie przezroczysty, z całą platformą Javy (istnieje co prawda implementacja Scali dla platformy .NET ale jest jeszcze niedojrzała). W Scali mamy też wygodną konsolę do interaktywnego testowania kodu (tak jak to jest w Pythonie i Ruby). W Scali każda wartość jest obiektem, każda funkcja zwraca wartość. Zatem każda funkcja też jest obiektem (first class object). Funkcje są też obiektami wyższego rzędu (higher order kinds), można je zagnieżdżać, przekazywać w parametrach, a nawet stosować mechanizm dziedziczenia.
Java jako platforma i jako język
Słowo “Java” oznacza z jednej strony język programowania, z drugiej platformę JVM. Sam język Java jest co prawda prosty, ale też dosyć toporny. Nic dziwnego, że pada wiele słów krytyki pod jego adresem. Co innego platforma Javy. JVM jest dopracowana, stabilna i szybka (ze swoim HotSpotem potrafi pobić kod napisany w języku C). Biblioteki są dojrzałe i używane masowo w biznesie i instytucjach finansowych. Każda nowość błyskawicznie znajduje swoje odzwierciedleniew Javie (ilość projektów w Javie wielokrotnie przewyższa to, co jest dostępne dla platformy .NET). Platforma Javy mocno prze do przodu i zwiększa stale swoją pozycję. Wydaje się tylko, że sam język nie za bardzo nadąża za tymi zmianami.
Gdy Microsoft “wyskoczył” ze swoją koncepcją .NET umożliwiającą tworzenie aplikacji na tą platformę w kilku językach a nie “jedynym prawidłowym”, można było odebrać to jako prztyczek w nos dla starej doktryny lansowanej kiedyś w środowisku Javy (jako jednego języka do wszystkiego). W rzeczy samej, patrząc dziś, Microsoft ze swymi językami zgodnymi z CLR niczym specjalnym nie zachwyca. Po pierwsze, istnieje dziś już ok. 240 różnych języków zgodnych z platformą Javy (jakieś 5x tyle co dla CLR). Javowe implementacje powstały nawet do języków dynamicznie typowanych (Groovy) oraz takich jak Python (Jython), Ruby (JRuby), JavaScript (Rhino) czy PHP (Quercus).
Po drugie, w przeciwieństwie do .NET, Java jest prawdziwie multiplatformowa (istnieje co prawda projekt Mono, ale wciąż odstaje jakością od windowsowego .NET). Mówiąc krótko, platforma Javy trzyma się mocno. Ale sam język jest przestarzały i wszyscy się zastanawiają nad tym, co by mogło go zastąpić. Moim zdaniem, coraz więcej wskazuje, że tym językiem może być Scala. (Swoją drogą język Java tak całkiem to pewnie nigdy nie zniknie. Raczej zredukuje się do miana assemblera XXI wieku. Zresztą już dziś tak wielu myśli o Javie, jako o assemblerze dla JVM)
Scala jako pure OOP
Scala jest językiem obiektowym w stopniu większym niż Java. W Scali nie ma sztucznego podziału na obiekty i typy prymitywne (int, float, string). Każda wartość jest w Scali pełnym obiektem (choć pod spodem kompilator Scali, tam gdzie się da, sam dokonuje inteligentnych konwersji na javowe prymitywy).
Każdy operator jest też metodą. Np. zapis 1 + 2 odpowiada dokładnie temu samemu co (1).+(2). Innymi słowy obiekt “1” używa tu metody ”+” operującej na parametrze będącym obiektem “2”. A to, że można opuścić nawiasy związane jest z tzw. notacją operatorową jaka jest możliwa Scali. O ile Ruby pozwala opuszczać nawiasy w niektórych sytuacjach (w definicji i wywołaniu metody), to Scala pozwala na opuszczanie ich w wypadku każdej metody pomiędzy dwoma wyrażeniami. W efekcie Scala pozwala na pisanie testów BDD przypomijących jeszcze bardziej język naturalny niż RSpec do Ruby’ego (też scalaz, scalatest). Także tworzenie nowych języków specjalnego zastosowania, tzw. DSL’i, dzięki notacji operatorowej wyglądają w Scali ładniej niż w Ruby. W ramach żartu pokazującego elastyczność składni Scali napisano nawet interpreter języka BASIC.
W Scali, w przeciwieństwie do Javy, nie ma też statycznych metod ani zmiennych (żałosne jest to, że tak nieobiektowe konstrukcje w ogóle wprowadzono do PHP). Zamiast nich Scala używa singletonów i metod klasowych. Oczywistą przewagą takiego podejścia jest m.in. możliwość dziedziczenia.
Podczas gdy Ruby i Python trzymają definicje metod klasowych i instancji razem, Scala trzyma je zgrupowane oddzielnie.
# Python:
class C(Object):
def metoda_instancji(self):
return "Jestem metodą instancji"
@classmethod
def metoda_klasowa(cls):
return "Jestem metodą klasową"
print C.metoda_klasowa()
print C().metoda_instancji
# Ruby:
class C
def self.metoda_klasowa
"Jestem metodą klasową"
end
def metoda_instancji
"Jestem metodą instancji"
end
end
puts C.metoda_klasowa
puts C.new.metoda_instancji
# Scala
class C {
def metoda_instancji =
"Jestem metodą instancji"
}
object C {
def metoda_klasowa =
"Jestem metodą klasową"
}
println((new C).metoda_instancji)
println(C.metoda_klasowa)
Scala jako język statycznie typowany z inteligentną inferencją typów
Scala jest językiem statycznie typowanym. Z pewnością ucieszy to tych, co nie chcą pisać tony dodatkowych testów jednostkowych. Statyczna kontrola typów zapewnia większą kontrolę nad kodem. Można się zdziwić, jakim cudem przedstawiony wyżej kod działa, skoro nie ma w nim żadnej deklaracji typów. Właściwie to powyższa deklaracja klas i singletonu formalnie wygląda tak
class C {
def metoda_instancji(): String =
return "Jestem metodą instancji";
}
object C {
def metoda_klasowa(): String =
return "Jestem metodą klasową";
}
Jednak nie trzeba pisać tak rozwleklego kodu. Scala posiada wyrafinowany mechanizm inferencji typów pozwalający na ograniczenie niepotrzebnego deklarowania typów w wypadkach kiedy kompilator jest w stanie to sobie samemu wywnioskować (podobną inferencję typów stosują inne języki, np. Haskell).
W Scali także (podobnie jak w Ruby czy JavaScript) końcowe średniki zamykające wyrażenie można pominąć, nawiasy dla metod bez parametrowych są opcjonalne i nie trzeba używać komendy return aby zwrócić wartość z metody. W Scali (podobnie jak w Ruby) zawsze zwracana jest ostatnia wartość wyrażenia.
Scala, podobnie jak Java i Ruby, nie posiada dziedziczenia wielobazowego. W przeciwieństwie do Javy, Scala nie posiada interfejsów. I bardzo dobrze, bo ich przydatność jest wysoce wątpliwa (oczywiście do PHP5 dodano bezmyślnie te nieszczęsne interfejsy). Ich główną wadą jest to, że nie mogą posiadać żadnej implementacji ani deklaracji zmiennych. Z kolei klasy abstrakcyjne w Javie mogą posiadać częściową implementację, ale nie można dziedziczyć po wielu klasach abstrakcyjnych tylko po interfejsach które nie mają żadnej implementacji. Bardzo niewygodne. Scala rozwiązuje ten problem za pomocą traits. Traits działają podobnie do modułów w Ruby. Nie mają konstruktorow ani nie można stworzyć ich instancji więc odpada efekt konfliktu kolejności konstruktorów. Traits mogą jednak podlegać dziedziczeniu, mogą mieć częściową lub pełną implementację metod, oraz mogą posiadać zadeklarowane zmienne. I każda klasa może dziedziczyć po kilku traitsach. Proste i wygodne.
Niejawne konwersje (implicit conversion)
Idea niejawnych konwersji nie jest w zasadzie niczym nowa. Występuje już w języku C. Gdy dodawane są dwie zmienne o typach short i long to ta pierwsza zostaje domyślnie przekształcona do long. Nie trzeba wykonywać żadnego jawnego rzutowania typu.
Scala idzie dalej. Za pomocą mechanizmu implicit conversions potrafi uzyskać efekty podobne jak w Ruby, ale bardziej bezpieczne. Zobaczmy jednak wpierw jak to się robi Ruby (który wykorzystuje swoją koncepcję otwartych klas).
class Integer
def kb
self * 1024
end
def mb
self * 1024 * kb
end
end
class String
def welcome
"Hello " + self
end
end
puts 10.kb # => 10240
puts 10.mb # => 10485760
puts "Jarek".welcome # => Hello Jarek
Co prawda Ruby potrafi “zamrozić” swoje obiekty uniemożliwiając ich modyfikację
Fixnum.freeze
class Integer
def kb
self * 1024
end
end
# => TypeError: can't modify frozen class
ale technika ta (zwana też trochę niepoprawnie jako “monkey patching”) jest używana dosyć powszechnie w Ruby. Główną jej wadą jest to, że takie zmiany w klasach są globalne i to może prowadzić do różnych nieporozumień w innych częściach kodu.
Scala nie ma tego problemu. Pozwala na uzyskanie podobnych efektów bez konieczności globalnych zmian w obiekcie. Zamiast otwierania klas, Scala dodaje definicje niejawnych konwersji. Jeśli wywołujemy metodę obiektu której on nie posiada, Scala patrzy na leksykalny kontekst definicji dostępnych konwersji dla danego typu (jak pamietamy, Scala jest językiem statycznie typowanym). Jeśli znajdzie sygnaturę metody taką jak nazwa wywoływanej metody, użyje jej tak jakby należała do obiektu.
class StrConvert(s: String) {
def welcome = "Hello " + s
}
class IntConvert(x: Int) {
def kb = 1024 * x
def mb = x * 1024 * kb
}
implicit def str_convert(s: String) = new StrConvert(s)
implicit def int_convert(i: Int) = new IntConvert(i)
println(10.kb)
println(2 mb)
println("Jarek" welcome)
Kropki w wywołaniach metod są oczywiście w Scali opcjonalne ze względu na notację operatorową.
Niejawne konwersje Scali przydają się w wypadku korzystania z cudzego kodu. Nie musimy (tak jak w Ruby) ingerować i zmieniać cudzych bibliotek. Zamiast tego wystarczy dodać lokalne definicje niejawnych konwersji typów. Przykładem toporności i ograniczeń Javy jest konieczność korzystania z innej składni do operacji na typach BigInteger. Jeśli początkowo pisaliśmy kod używający zwykłych typów int to w wypadku kiedy musi on operować na większych liczbach, czeka nas niezła refaktoryzacja kodu a kod mógłby wyglądać mniej więcej tak jak poniższy kod (przykład dotyczy składni Scali w ktorej też można pisać po “javowemu”)
import java.math.BigInteger
def factorial(x: BigInteger): BigInteger =
if (x == BigInteger.ZERO)
BigInteger.ONE
else
x.multiply(factorial(x.subtract(BigInteger.ONE)))
Dzięki możliwościom implicit conversion, znacznie wygodniej jest pisać tak:
def factorial(x: BigInt): BigInt =
if (x == 0) 1 else x * factorial(x - 1)
Warto zwrócić uwagę, że to nie jest jakaś dodana nowa składnia do Scali. Wrapper do BigInt jest stworzony w bibliotece. Nie trzeba żadnego przebudowania kodu Scali tak, jak to musi być uczynione w innych językach.
Scala jako język funkcyjny (FP)
Mam wrażenie, że programowanie funkcyjne przeżywa jakiś swój renesans. Od jakiegoś czasu więcej słychać o Erlangu, Clojure (świetny dialekt Lispa w Javie), Haskellu czy F#. Zalety programania funkcyjnego w stosunku do programowania imperatywnego są znane. Przede wszystkim, brak zmiennych i brak efektów ubocznych. Odpadają problemy związane z synchronizacją wątków, semaforami i blokowaniem dostępu do wspólnych obszarów pamięci. Kod pisany funkcyjnie znacznie łatwiej jest też testować. Funkcja niczym ma tylko jedno wejście i jedno wyjście. Nic innego ukradkiem nie zmieni. Programowanie funkcyjne jest też prostsze, choć osoby spaczone latami spędzonymi na programowaniu imperatywnym, mogą mieć na początku pewne problemy z przestawieniem sposobu myślenia. Np. w typowo funkcyjnym podejściu nie ma w ogóle pętli w programie. Zastępuje ją rekursja (która nie przepełnia stosu, jest to tzw. tail recursion)
Przykład różnic w podejściu imperatywnym oraz funkcyjnego dla Ruby’ego i Scali.
Ruby:
# Podejście imperatywne:
class Imper
attr_accessor :x, :y
def initialize(x,y)
@x, @y = x, y
end
def move_by(dx, dy)
@x += dx
@y += dy
end
def show
puts "x:#{x}, y:#{y}"
end
end
x = Imper.new(1,2)
x.move_by(3,4)
x.show
# To samo funkcyjnie:
class Func
attr_reader :x, :y
def initialize(x,y)
@x, @y = x, y
end
def move_by(dx, dy)
Func.new(@x+dx, @y+dy)
end
def show
puts "x:#{x}, y:#{y}"
end
end
x = Func.new(1,2)
x = x.move_by(3,4)
x.show
# lub to samo, ale bez tworzenie zmiennych:
Func.new(1,2).move_by(3,4).show
Scala:
// Kod w stylu imperatywnym:
class Imper(x: Int, y: Int) {
var iks: Int = x
var igrek: Int = y
def move_by(dx: Int, dy: Int) {
iks += dx
igrek += dy
}
def show {
println(iks,igrek)
}
}
val p = new Imper(1,2)
p.move_by(3,4)
p.show
// i to samo funkcyjnie:
class Func(x: Int, y: Int) {
val iks: Int = x
val igrek: Int = y
def move_by(dx: Int, dy: Int) = new Func(iks+dx, igrek+dy)
def show {
println(iks,igrek)
}
}
(new Func(1,2)).move_by(3,4).show
Obiekty deklarowane jako val są niemutowalne, nie można zmienić ich wartości (odpowiadają javowej deklaracji final). Z kolei deklaracja var oznacza zmienne mutowalne, mogące zmienić swoją wartość.
Kod pisany funkcyjnie, może być znacznie krótszy, prostszy i tym samym mniej podatny na błędy. Zobaczmy, jak bardzo można zredukować w Scali kod napisany w stylu imperatywnym do funkcyjnego (przykład z książki “Programming in Scala”)
def printArgs(args: Array[String]): Unit = {
var i = 0
while (i < args.length) {
println(args(i))
i += 1
}
}
Pozbywamy się zmiennej var:
def printArgs(args: Array[String]): Unit = {
for (arg <- args)
println(arg)
}
To samo, inaczej wyrażone:
def printArgs(args: Array[String]): Unit = {
args.foreach(println)
}
Metoda nie jest jeszcze “czysto” funkcyjna bo posiada efekt uboczny w postaci drukowania wartości w funkcji println. Pozbywamy się więc i jej.
def formatArgs(args: Array[String]) = args.mkString("\n")
Wywołanie:
println(formatArgs(args))
Warunki zwracające wynik
W Scali warunki logiczne także zwracają wartość. Weźmy na przykład kwestię operatora trynarnego. W wielu językach można spotkać (odziedziczoną po C) następującą składnię:
wynik = warunek ? wyrazenie1 : wyrazenie2
Przez wiele lat w Pythonie trzeba było pisać w taki sposób:
if warunek:
wynik = wyrazenie1
else:
wynik = wyrazenie2
Pod naciskiem społeczności, Guido van Rossum dodał w końcu odpowiednik takiej konstrukcji i teraz można pisać tak:
wynik = warunek if wyrazenie1 else wyrazenie2
Tymczasem w Scali taki problem jest zupełnie sztuczny. Skoro warunek zwraca wynik, to nie trzeba wymyślać żadnej nowej składni naśladującej język C. Wystarczy po prostu napisać:
val wynik = if (warunek) wyrazenie1 else wyrazenie2
Ogólnie, cała Scala promuje podejście funkcyjne, ale nie zmusza do niego. Scala nie stosuje oddzielnych nazw dla wersji mutowalnej i niemutowalnej. Dzięki temu unika się niepotrzebnego refactoringu kodu w wypadku potrzeby zmiany jednej wersji na drugą.

Domyślnie typy są niemutowalne.
scala> val x = Map[Int, String]()
# => x: scala.collection.immutable.Map[Int,String] = Map()
Aby mieć tym mutowalny, trzeba to jawnie wskazać.
scala> import scala.collection.mutable.Map
scala> val x = Map[Int, String]()
# => x: scala.collection.mutable.Map[Int,String] = Map()
Lift – komponentowy framework w Scali
Mówiąc o Scali nie sposób nie wspomnieć o świetnym frameworku webowym jakim jest Lift. Framework ten inspirowany jest różnymi frameworkami (głównie Wicket’em ale też Django, Rails czy smalltalkowym Seaside). Lift jest przede wszystkim frameworkiem komponentowym (odcina się tu od klasycznego modelu MVC promowanego przez Rails). Podejście komponentowe ma przewagę nad MVC, dużo lepiej się nadaje do złożonych aplikacji (innym przykładem dobrego podejścia komponentowego jest pythonowy CMS – Plone. Zobacz też SweetScala – konkurencyjny framework w Scali)
Podsumowując.
Scala to nowoczesny język będący hybrydą pure OOP i FP, statycznie typowany ale z inteligentną inferencją typów, o bardzo elastycznej składni i kompilowana do bytecodu JVM. Dzięki zaawansowanej inferencji typów i implicit conversions nie odczuwa się narzutu związanego z faktem że Scala jest statycznie typowana (a dostaje zalety ściślejszej kontroli kodu). Scala nie ma operatorów, operatory są zwykłymi metodami definiowanymi w bibliotekach.
Jednak tym co jest największym atutem Scali to jej minimalizm składniowy. Scala umożliwia tworzenie nowych typów i struktur językowych bez konieczności zmian w samej zasadniczej składni języka. To oznacza, źe podobnie jak Lisp, Scala nie ma praktycznie żadnych ograniczeń w rozwoju. Tym samym Scala jest w stanie przetrwać różne tendencje i upodobania składniowe i “przeskalować” się do nowych potrzeb.
Scalę używa już gdzieś parę banków w UK. Na Scalę niedawno przeszedł Twitter, ma wkrótce przejść też Github.
Linki