25
1 ŠTO POSLIJE PASCALA? PA … SCALA! SAŽETAK Sedamdesetih i osamdesetih godina prošlog stoljeća, za obučavanje osnova programiranja najčešće se koristio programski jezik Pascal. Pascal nije bio samo "akademski" jezik, u njemu se pisao čak i sistemski softver (pa i cijeli operacijski sustavi). No, programski jezik C (koji je Pascalov "vršnjak") pomalo je istisnuo Pascal iz oba područja (ne u potpunosti). Za obučavanje os nova programiranja, najčešće se koristi C/C++, a nešto manje Java. Već duže vrijeme na Java virtualnom stroju (JVM) možemo izvršavati i programe koji nisu izvorno napisani u Javi, već u nekom drugom jeziku koji se može prevesti u Java bytecode. Jedan od njih je programski jezik Scala, čija je prva verzija izaš la prije 10 godina (2003.). Autor Scale je profesor Martin Odersky, a zanimljivo je da je doktorirao (krajem osamdesetih) kod profesora Niklausa Wirtha, autora Pascala. Scala je prije svega objektni programski jezik, ali tako nadograđen da ima i značajne funkci jske osobine, pa se može reći da je to i funkcijski programski jezik. Mišljenja smo da je Scala vrlo perspektivan programski jezik za programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske osobine, te konkurentno programiranje u jeziku Scala. ABSTRACT In 1970’s and 1980’s, Pascal was the programming language most commonly used to teach the basics of programming. Pascal was not just an "academic" language, it was also used for writing system software (and even entire operating systems). However, the programming language C (which is Pascal's "peer") has gradually pushed out Pascal from both areas (not completely). To teach the basics of programming, the most commonly used languages are C / C ++, somewhat less Java. For a long time now, even the programs which have not been originally written in Java, but in other languages that can be translated into Java bytecode, can be executed on Java virtual machine (JVM), One of them is Scala programming language, whose first version came out 10 years ago (2003). Scala's author is professor Martin Odersky, who, interestingly enough, won his doctorate (in late eighties) by professor Niklaus Wirth, the author of Pascal. Scala is primarily an object programming language, but upgraded in such a way that it has significant functional properties, so you can say that it is also a functional programming language. We think that Scala is a very promising programming language for programming on JVM, but also a very good language for modern teaching of basic programming skills. The paper presents some object properties, functional properties, and concurrent programming in language Scala. 1. UVOD Sedamdesetih i osamdesetih godina prošlog stoljeća, za obučavanje osnova programiranja najčešće se koristio programski jezik Pascal. Pascal nije bio samo "akademski" jezik, u njemu se pisao čak i sistemski softver (pa i cijeli operacijski sustavi). No, programski jezik C (koji je Pascalov "vršnjak") pomalo je istisnuo Pascal iz oba područja (ne u potpunosti). Za obučavanje osnova programiranja, najčešće se koristi C/C++, a nešto manje Java. Već duže vrijeme na Java virtualnom stroju (JVM) možemo izvršavati i programe koji nisu izvorno napisani u Javi, već u nekom drugom jeziku koji se može prevesti u Java bytecode. Jedan od njih je programski jezik Scala, čija je prva verzija izašla prije 10 godina (2003.). Autor Scale je profesor Martin Odersky, a zanimljivo je da je doktorirao (krajem osamdesetih) kod profesora Niklausa Wirtha, autora Pascala. Scala je prije svega objektni programski jezik, ali tako nadograđen da ima i značajne funkcijske osobine, pa se može reći da je to i funkcijski programski jezik. Funkcijski jezici nisu novost. Prvi funkcijski jezik Lisp nastao je 1958., svega godinu dana nakon prvog višeg programskog jezika Fortran. Funkcijski jezici (Lisp i njegove izvedenice) su, unatoč puno slabijoj zastupljenosti u odnosu na imperativne jezike, uvijek imali manju, ali jaku zajednicu koja ih je podržavala. Ta je zajednica bila (i jeste) naročito snažna u SAD-u, posebno za potrebe programiranja aplikacija za umjetnu inteligenciju, dok se u Evropi i Japanu za te namjene puno više koristi logički programski jezik Prolog i njegove izvedenice. I na JVM-u postoji više funkcijskih programskih jezika. Osobno smo mišljenja da su statički programski jezici (tj. programski jezici sa statičkom provjerom tipova, kod kojih se provjere tipova rade kod kompajliranja) sigurniji za profesionalno programiranje od dinamičkih programskih jezika (kod kojih se greška javlja tek kod izvođenja programa). Scala (kao i Fortran, Algol, Pascal, C/C++, Eiffel, PL/SQL, funkcijski jezik Haskell, Java, C# ...) je statički programski jezik (dok su dinamički Lisp, izvorni Prolog, Smalltalk, Phyton, Ruby, Groovy ...). Za statičke jezike se često govori da "sputavaju programera", ali za Scalu mnogi kažu da ona pruža (statičku) sigurnost i fleksibilnost dinamičkog jezika. Mišljenja smo da je Scala vrlo perspektivan programski jezik za programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske osobine, te konkurentno programiranje u jeziku Scala.

ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

  • Upload
    others

  • View
    1

  • Download
    0

Embed Size (px)

Citation preview

Page 1: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

1

ŠTO POSLIJE PASCALA? PA … SCALA!

SAŽETAK

Sedamdesetih i osamdesetih godina prošlog stoljeća, za obučavanje osnova programiranja najčešće se koristio programski

jezik Pascal. Pascal nije bio samo "akademski" jezik, u njemu se pisao čak i sistemski softver (pa i cijeli operacijski sustavi). No, programski jezik C (koji je Pascalov "vršnjak") pomalo je istisnuo Pascal iz oba područja (ne u potpunosti). Za obučavanje osnova programiranja, najčešće se koristi C/C++, a nešto manje Java.

Već duže vrijeme na Java virtualnom stroju (JVM) možemo izvršavati i programe koji nisu izvorno napisani u Javi, već u nekom drugom jeziku koji se može prevesti u Java bytecode. Jedan od njih je programski jezik Scala, čija je prva verzija izašla prije 10 godina (2003.). Autor Scale je profesor Martin Odersky, a zanimljivo je da je doktorirao (krajem osamdesetih) kod profesora Niklausa Wirtha, autora Pascala. Scala je prije svega objektni programski jezik, ali tako nadograđen da ima i značajne funkci jske osobine, pa se može reći da je to i funkcijski programski jezik. Mišljenja smo da je Scala vrlo perspektivan programski jezik za programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske osobine, te konkurentno programiranje u jeziku Scala.

ABSTRACT

In 1970’s and 1980’s, Pascal was the programming language most commonly used to teach the basics of programming.

Pascal was not just an "academic" language, it was also used for writing system software (and even entire operating systems). However, the programming language C (which is Pascal's "peer") has gradually pushed out Pascal from both areas (not completely). To teach the basics of programming, the most commonly used languages are C / C ++, somewhat less Java.

For a long time now, even the programs which have not been originally written in Java, but in other languages that can be translated into Java bytecode, can be executed on Java virtual machine (JVM), One of them is Scala programming language, whose first version came out 10 years ago (2003). Scala's author is professor Martin Odersky, who, interestingly enough, won his doctorate (in late eighties) by professor Niklaus Wirth, the author of Pascal. Scala is primarily an object programming language, but upgraded in such a way that it has significant functional properties, so you can say that it is also a functional programming language. We think that Scala is a very promising programming language for programming on JVM, but also a very good language for modern teaching of basic programming skills. The paper presents some object properties, functional properties, and concurrent programming in language Scala.

1. UVOD Sedamdesetih i osamdesetih godina prošlog stoljeća, za obučavanje osnova programiranja najčešće

se koristio programski jezik Pascal. Pascal nije bio samo "akademski" jezik, u njemu se pisao čak i sistemski softver (pa i cijeli operacijski sustavi). No, programski jezik C (koji je Pascalov "vršnjak") pomalo je istisnuo Pascal iz oba područja (ne u potpunosti). Za obučavanje osnova programiranja, najčešće se koristi C/C++, a nešto manje Java.

Već duže vrijeme na Java virtualnom stroju (JVM) možemo izvršavati i programe koji nisu izvorno napisani u Javi, već u nekom drugom jeziku koji se može prevesti u Java bytecode. Jedan od njih je programski jezik Scala, čija je prva verzija izašla prije 10 godina (2003.). Autor Scale je profesor Martin Odersky, a zanimljivo je da je doktorirao (krajem osamdesetih) kod profesora Niklausa Wirtha, autora Pascala. Scala je prije svega objektni programski jezik, ali tako nadograđen da ima i značajne funkcijske osobine, pa se može reći da je to i funkcijski programski jezik.

Funkcijski jezici nisu novost. Prvi funkcijski jezik Lisp nastao je 1958., svega godinu dana nakon prvog višeg programskog jezika Fortran. Funkcijski jezici (Lisp i njegove izvedenice) su, unatoč puno slabijoj zastupljenosti u odnosu na imperativne jezike, uvijek imali manju, ali jaku zajednicu koja ih je podržavala. Ta je zajednica bila (i jeste) naročito snažna u SAD-u, posebno za potrebe programiranja aplikacija za umjetnu inteligenciju, dok se u Evropi i Japanu za te namjene puno više koristi logički programski jezik Prolog i njegove izvedenice. I na JVM-u postoji više funkcijskih programskih jezika.

Osobno smo mišljenja da su statički programski jezici (tj. programski jezici sa statičkom provjerom tipova, kod kojih se provjere tipova rade kod kompajliranja) sigurniji za profesionalno programiranje od dinamičkih programskih jezika (kod kojih se greška javlja tek kod izvođenja programa). Scala (kao i Fortran, Algol, Pascal, C/C++, Eiffel, PL/SQL, funkcijski jezik Haskell, Java, C# ...) je statički programski jezik (dok su dinamički Lisp, izvorni Prolog, Smalltalk, Phyton, Ruby, Groovy ...). Za statičke jezike se često govori da "sputavaju programera", ali za Scalu mnogi kažu da ona pruža (statičku) sigurnost i fleksibilnost dinamičkog jezika.

Mišljenja smo da je Scala vrlo perspektivan programski jezik za programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske osobine, te konkurentno programiranje u jeziku Scala.

Page 2: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

2

2. NASTANAK JEZIKA SCALA Programski jezik Scala kreirao je Martin Odersky, profesor na Ecole Polytechnique Fédérale de

Lausanne (EPFL). Kako je naveo u seriji od četiri intervjua [4], profesor Odersky je još kao student početkom 80-ih, zajedno sa jednim kolegom, napisao jedan od prvih kompajlera za jezik Modula-2. Kompajler je kupila firma Borland, koja ih je pozvala da rade kod njih. Odersky se odlučio za akademsku karijeru i krajem 80-ih doktorirao na ETH Zürich kod profesora Niklausa Wirtha (kreatora Pascala i Module-2). Nakon toga naročito se bavio istraživanjima u području funkcijskih jezika, zajedno sa kolegom Philom Wadlerom (jednim od dva glavna kreatora funkcijskog jezika Haskell). Kada je izašla Java, Odersky i Wadler su 1996. napravili jezik Pizza nad JVM-om, koji je uključivao tri mogućnosti iz funkcijskog programiranja: generičke klase (generics), funkcije višeg reda (higher-order functions) i podudaranje (sparivanje) uzorka (pattern matching). To je bila jedna od prvih implementacija ne-Java jezika nad JVM-om. Na temelju projekta Pizza, napravili su 1997./98. Generic Java (GJ), koji je uveden u Javu 5 (malo ga je nadopunio Gilad Bracha, sa wildcardsima). Dok je za primjenu GJ-a Sun čekao skoro 6 godina, odmah su preuzeli Java kompajler koji je Odersky napravio za GJ. Taj se kompajler koristi od Jave 1.3 dalje.

Nakon rada sa GJ-om, Odersky je postao profesor na EPFL i mogao se posvetiti razvoju novog jezika, koji bi bio vezan za JVM i Java biblioteke, ali koji bi bio bolji od Jave. Prvo je kreirao jezik Funnel. Zaključio je da je taj jezik previše akademski, pa je 2002. počeo raditi novi jezik Scala. Scala je tako nazvana kako bi se naglasila njena skalabilnost. Prva javna verzija izašla je 2003., a relativno značajni redizajn napravljen je 2006. (trenutačna verzija je 2.10). Od tada, Scala se sve više koristi u praksi, tako da je došla među prvih 50 najkorištenijih jezika, sa tendencijom da se probije među prvih 20. Zanimanje za Scalu naročito se povećalo kada je Twitter prebacio glavne dijelove svojih programa (koji rade procesiranje poruka) iz jezika Ruby u Scalu.

Odersky je naglasio da nije želio da Scala bude 100% kompatibilna sa Javom pod svaku cijenu.

Npr. odustao je od toga da Scala nizovi (arrays) budu kovarijantni (covariant) kao u Javi. Inače, Odersky naglašava da je odluka Java kreatora da (u nedostatku generičkih klasa, na početku razvoja Jave) nizovi budu kovarijantni, bila jedna od najvećih grešaka kod razvoja Jave. Kreatori Jave su brzo toga postali svjesni, ali bilo je već kasno. Scala je kreirana kao čisti objektno-orijentirani jezik (kao i Eiffel, dok Java nije čisti OOPL), i u nju su uvedene neke objektno-orijentirane mogućnosti koje Java nema. Osim toga, na temelju objektno-orijentiranih mogućnosti izgrađene su i brojne funkcijske mogućnosti, tako da je Scala i funkcijski jezik (ali nije čisti). Objektno-orijentirani pristup dobar je kada se skup klasa prirodno proširuje. Funkcijski pristup pogodniji je kada je skup struktura relativno fiksan, ali se žele uvesti nove operacije nad postojećim strukturama, gdje je najbolji pristup podudaranje uzorka (pattern matching). Sa funkcijskim osobinama došle su i neke osobine koje su vrlo pogodne za konkurentno programiranje.

Odersky je u intervjuima spominjao i neke relativno manje važne odluke, vezane za sintaksu. Npr., odlučio je zadržati C / C++ / Java / C# vitičaste zagrade umjesto npr. riječi BEGIN / END ili sličnih, jer mu se činilo da su vitičaste zagrade sasvim dobre. No, želio je da se pridruživanje varijabli označava kao u jezicima Pascal / Modula-2 / Ada ... , tj. pomoću := (a da se znak = ostavi za označavanje jednakosti, kao u matematici), a ne kao u jezicima Fortran (on je "krivac" za takav stil) / C / C++ / Java / C# ... (gdje se onda za označavanje jednakosti koristi ==). Ipak je odustao, nakon žestokih negativnih reakcija programera naviklih na Java stil. Za razliku od toga, nije odustao od sintakse da se ime varijable piše prije tipa varijable (kao npr. u Pascalu: brojcanaVarijabla: int), a ne obrnuto (kao npr. u Javi: int brojcanaVarijabla), između ostalog i zato što bi inače bilo vrlo otežano parsiranje Scala koda.

Scala je izvrstan jezik i za pisanje DSL-ova (Domain-Specific Language), programskih jezika za

specifičnu problemsku domenu. Profesor Odersky je krajem 2010. od European Research Councila (ERC) dobio Advanced Investigator Grant (oko 2,5 milijuna eura za projekt u trajanju od 5 godina, 2011.-2016.) za svoj projekt vezan za DSL-ove za paralelno programiranje pisane u Scali. Istu potporu dobio je krajem 2011. (za razdoblje 2012.-2017.) profesor sa ETH Zürich, Bertrand Meyer, kreator jezika Eiffel, za svoj projekt "Concurrency Made Easy", koji se temelji na SCOOP metodi (SCOOP metoda detaljno je opisana u [2]). Zanimljivo je da su i Eiffel (kreiran 1986., skoro deset godina prije Jave) i Scala vrlo rigorozno matematički specificirani, za razliku od Jave. Eiffel je i ISO standardiziran, što Java i Scala za sada nisu. Scala podržava statičku provjeru tipova, kao i C++, Eiffel, Haskell, Java, C# ... , za razliku od npr. jezika Python, Ruby, Groovy, Clojure ... , kod kojih se greška u tipovima pokaže tek kod izvođenja programa.

Značajno je da i kreatori drugih ("konkurentnih") jezika imaju vrlo visoko mišljenje o Scali:

"If I were to pick a language to use today other than Java, it would be Scala." - James Gosling, creator of Java

Page 3: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

3

"Scala, it must be stated, is the current heir apparent to the Java throne. No other language on the JVM seems as capable of being a "replacement for Java" as Scala, and the momentum behind Scala is now unquestionable. While Scala is not a dynamic language, it has many of the characteristics of popular dynamic languages, through its rich and flexible type system, its sparse and clean syntax, and its marriage of functional and object paradigms." - Charles Nutter, creator of JRuby "I can honestly say if someone had shown me the Programming in Scala book by Martin Odersky, Lex Spoon & Bill Venners back in 2003 I'd probably have never created Groovy." - James Strachan, creator of Groovy.

3. SCALA KAO OBJEKTNI PROGRAMSKI JEZIK 3.1. Neke osobine

U Scali se definicija funkcije (ili procedure) radi na malo drugačiji način nego u Javi. Ne samo da se

koristi ključna riječ def, već se (kao i kod varijabli) prvo navodi ime funkcije, a tip funkcije slijedi na kraju, iza parametara funkcije. Tip funkcije nije nužno uvijek navesti, jer ga Scala kompajler može (kao i tipove drugih elemenata programskog koda) izvesti iz drugih podataka. To je tzv. izvođenje tipova ili zaključivanje o tipovima (type inference), što statički jezik Scala čini fleksibilnim kao da je jezik dinamički. Iza (neobaveznog) tipa funkcije, slijedi znak jednakosti (ako je riječ o funkciji). U funkciji se ne mora pisati riječ return, jer se podrazumijeva da zadnja izvršena operacija vraća vrijednost funkcije. Sljedeća slika 3.1 prikazuje osnovnu formu Scala funkcije:

Slika 3.1. Osnovna forma definicije funkcije u Scali; Izvor: [5]

Funkcije se u Scali mogu gnijezditi, tj. mogu se raditi lokalne funkcije (kao što ima i PL/SQL). Slijedi

primjer iz [5], koji za zadanu datoteku ispisuje retke veće od zadane širine: def processFile(filename: String, width: Int) {

def processLine(filename: String, width: Int, line: String) {

if (line.length > width)

println(filename +": "+ line)

}

val source = Source.fromFile(filename)

for (line <- source.getLines()) {

processLine(filename, width, line)

}

}

Primjećuje se da se kod definicije varijable source koristi ključna riječ val (kao value), za razliku od

ključne riječi var (kao variable). Obje ključne riječi označavaju varijablu, ali varijabla označena sa val je imutabilna, tj. nakon jednokratnog pridruživanja vrijednosti više se ne može mijenjati - kao Java final. Također, primjećuje se da u Scali nije nužna upotreba separatora točka-zarez (osim u nekim vrlo specijalnim slučajevima).

Page 4: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

4

Funkcije u Scali, uz uobičajene pozicijske parametre, mogu imati i imenovane parametre (named parameters). Na taj način omogućeno je da se kod poziva funkcije parametri (netko kaže da su kod poziva to argumenti, a ne parametri) navedu drugačijim redom u odnosu na redoslijed parametara u definiciji funkcije. Također, Scala ima default parametre, koji su posebno korisni u kombinaciji sa imenovanim parametrima. Npr., kod poziva neke funkcije koja ima 5 parametara, ali su prvi i četvrti default, mogli bismo u pozivu navesti eksplicitno svih 5 parametara, ili samo drugi, treći i peti parametar (koji nemaju default vrijednosti) kao imenovane parametre, ali možemo navesti i default parametre, ako im želimo staviti ne-default vrijednost. U Scali poziv metode može imati varijabilan broj argumenata (varargs), ali to vrijedi za zadnji parametar metode (kao i od Jave 5).

Kao i Java, tako i Scala ima try – catch - finally blok za obradu iznimki. Za razliku od Jave, Scala nema tzv. kontrolirane iznimke (checked exceptions), tj. ne traži da se iznimka ili obradi, ili deklarira u throws klauzuli. Također, Scala try – catch – finally blok uvijek vraća vrijednost.

Kao i Java (i većina drugih jezika), Scala dopušta rekurziju. No, u ne-funkcijskim jezicima često se izbjegava rekurzija, zato što ona (uobičajeno) kod svakog poziva funkcije povećava stog (stack), čime se troši memorija, usporava rad, a program može i puknuti ako se stog prepuni. Zato se obično sljedeći rekurzivni kod iz [5]:

def approximate(guess: Double): Double =

if (isGoodEnough(guess)) guess

else approximate(improve(guess))

zamjenjuje sa nerekurzivnom varijantom, koja koristi petlju (u ovom primjeru while): def approximateLoop(initialGuess: Double): Double = {

var guess = initialGuess

while (!isGoodEnough(guess))

guess = improve(guess)

guess

}

Funkcijski jezici obilato koriste rekurziju, jer oni imaju optimizaciju kojom mogu izbjeći povećavanje

stoga kod tzv. repne rekurzije (tail recursion), tj. onda kada je zadnja operacija u kodu ("na repu" koda) poziv funkcije same. To može i Scala (uz neka ograničenja), tako da su u Scali dvije prethodne varijante praktički istovjetne. No, ako funkcija nije repno rekurzivna (tail-recursive), onda nema optimizacije. Npr. sljedeća funkcija nije repno rekurzivna, jer na kraju iza rekurzivnog poziva postoji još i dodatno zbrajanje (označeno crvenom bojom):

def boom(x: Int): Int =

if (x == 0) throw new Exception("boom!")

else boom(x - 1) + 1

Korištenje optimizacije kod repne rekurzije u Scali je ipak ograničeno, jer JVM skup instrukcija vrlo otežava implementaciju naprednih formi optimizacije rekurzivnih poziva. Npr., u slučaju dvije međusobno pozivajuće funkcije, gdje je repna rekurzija indirektna, Scala optimizator ne prepoznaje repnu rekurziju.

3.2. Klase Scala je čisti objektno-orijentirani jezik (ali nije čisti funkcijski jezik). Svaka vrijednost je objekt, a

svaka operacija je poziv metode (neke klase), kao što prikazuje sljedeća slika 3.2:

Slika 3.2. U Scali su sve operacije metode (neke klase); Izvor: [5]

Page 5: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

5

Dakle, obično zbrajanje brojeva 1 + 2 predstavlja poziv metode + nad objektom (1), a metodi se šalje parametar (2). Treba naglasiti da se pritom ništa ne gubi na performansama, jer Scala optimizator napravi kod koji je isto tako optimalan kao i Java kod koji koristi primitivne tipove (npr. int) koji nisu klase. Ponekad se može i obrnuto, tj. metoda se može pisati u "operatorskom" obliku. Npr., uz uobičajeni stil:

nekiObjekt.nekaMetoda(nekiParametar) u Scali se poziv metode može pisati bez točke, čak i bez zagrada (ako metoda ima 0 ili 1 parametar): nekiObjekt nekaMetoda nekiParametar Gledano tehnički, Scala nema operatore, već metode imenovane specijalnim simbolima (kao simbol

+), pa se mogućnost opterećenja operatora (operator overloading), koju Java nema, a npr. C++, Eiffel, C# imaju, rješava kao uobičajeno preopterećenje funkcija, kao u ovom primjeru iz [7], gdje je uvedena metoda + koja "glumi" operator za zbrajanje kompleksnih brojeva:

class Complex(val real: Int, val imaginary: Int) {

def +(operand: Complex): Complex = {

new Complex(real + operand.real, imaginary + operand.imaginary)

}

override def toString(): String = {

real + (if (imaginary < 0) "" else "+") + imaginary + "i"

}

}

val c1 = new Complex(1, 2)

val c2 = new Complex(2, -3)

val sum = c1 + c2

println("(" + c1 + ") + (" + c2 + ") = " + sum)

Prethodni primjer prikazuje i to da se u Scali koristi ključna riječ override za označavanje

nadjačavanja (naslijeđene) metode toString specifičnom varijantom klase Complex. Takvo rješenje ima i Eiffel, dok se u Javi može, ali ne mora, navesti odgovarajuća anotacija (uvedeno u Javi 5). Obavezno eksplicitno označavanje je sigurnije, jer se u Javi greškom može desiti da smo zapravo uveli novu metodu, sa drugačijom signaturom od one u nadklasi, umjesto da nadjačamo metodu nadklase.

Slika 3.3 prikazuje hijerarhiju klasa u Scali. Vidi se da sve klase nasljeđuju klasu Any, a nju nasljeđuju klasa AnyVal (koja je nadklasa za "primitivne" tipove) i klasa AnyRef, koja je zapravo Java klasa Object. Klasa Null je podklasa svih AnyRef podklasa, a klasa Nothing je podklasa svih klasa:

Slika 3.3. Hijerarhija Scala klasa; Izvor: [5]

Page 6: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

6

Sljedeći primjer iz [7] prikazuje nasljeđivanje klasa. Klasa Car nasljeđuje klasu Vehicle i dodaje novi atribut fuelLevel, te nadjačava metodu toString:

class Vehicle(val id: Int, val year: Int) {

override def toString(): String = "ID: " + id + " Year: " + year

}

class Car(override val id: Int, override val year: Int,

var fuelLevel: Int) extends Vehicle(id, year) {

override def toString(): String =

super.toString() + " Fuel Level: " + fuelLevel

}

val car = new Car(1, 2009, 100)

println(car)

Scala nema statička polja i statičke metode (kao ni Eiffel), jer to nije u skladu sa objektno-

orijentiranim pristupom. No, zato Scala ima singleton klase (klase koje imaju samo jednu instancu), koje se označavaju pomoću ključne riječi object, umjesto riječi class. Sljedeći primjer iz [7] prikazuje klasu Marker, čiji se objekti kreiraju pomoću singleton klase MarkerFactory:

class Marker(val color: String) {

println("Creating " + this)

override def toString(): String = "marker color " + color

}

object MarkerFactory {

private val markers = Map(

"red" -> new Marker("red"),

"blue" -> new Marker("blue"),

"green" -> new Marker("green")

)

def getMarker(color: String) =

if (markers.contains(color)) markers(color) else null

}

println(MarkerFactory getMarker "blue") //...

U prethodnom primjeru objekt MarkerFactory je samostalan (stand-alone) objekt. Međutim, Scala

omogućava i da se napravi objekt koji ima isto ime kao klasa, tzv. drugarski objekt (companion object). Time se omogućava da klasa i njen drugarski objekt mogu međusobno pristupati tuđim privatnim poljima i metodama, čime se postižu iste mogućnosti kao kod statičkih polja i metoda, ali na "čišći" način. Sljedeći primjer iz [7] prikazuje klasu Marker i njen drugarski objekt:

class Marker private (val color: String) {

override def toString(): String = "marker color " + color

}

object Marker {

private val markers = Map(

"red" -> new Marker("red"),

"blue" -> new Marker("blue"),

"green" -> new Marker("green")

)

def primaryColors = "red, green, blue"

def apply(color: String) =

if (markers.contains(color)) markers(color) else null

}

println("Primary colors are : " + Marker.primaryColors)

println(Marker("blue"))

println(Marker("red")) //...

Page 7: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

7

Scala ima n-torke (tuples) i višestruko pridruživanje (multiple assignments). Primjer iz [7]: def getPersonInfo(primaryKey: Int) = {

// Assume primaryKey is used to fetch a person's info...

// Here response is hard-coded

("Venkat", "Subramaniam", "[email protected]")

}

val (firstName, lastName, emailAddress) = getPersonInfo(1)

println("First Name is " + firstName) // ...

Scala može imati pakete unutar paketa. Default u Scali je public vidljivost, ali Scala omogućava puno

finiju granularnost vidljivosti (u paketu i klasi) u odnosu na Javu. Postoji i mogućnost da se vidljivost privatnih polja / metoda ograniči samo na određenu instancu klase (kao što imaju i Eiffel i Ruby).

3.3. Scala trait Scala nema pravo višestruko nasljeđivanje (multiple inheritance) kao što imaju C++ i Eiffel. Često se

kaže da je višestruko nasljeđivanje komplicirano, vjerojatno na temelju iskustva sa C++. Bertrand Meyer je uveo višestruko nasljeđivanje u Eiffel od početka (1986), dok je u C++ ono uvedeno naknadno (1989.), pa je vjerojatno zato izvedeno neoptimalno. Javlja se tzv. dijamantni problem (diamond problem), kad neka klasa nasljeđuje (barem) dvije klase koje pak nasljeđuju istu klasu-pretka (to liči na skicu dijamanta, otud ime problema), pa se ne može izbjeći dvostruko nasljeđivanje istoimenih metoda i polja. Eiffel je elegantno riješio taj problem, kao i općenitiji problem istih imena metoda i polja u klasama od kojih se nasljeđuje.

Kod dizajniranja Jave, odabralo se samo jednostruko nasljeđivanje klasa, a višestruko nasljeđivanje imaju samo Java sučelja (interface), koji su zapravo potpuno apstraktne klase (bez konkretnih metoda, tj. bez programskog koda). Budući da se ipak tokom vremena shvatilo da je višestruko nasljeđivanje korisno, u Scali je uveden programski konstrukt koji omogućava skoro isti efekt kao višestruko nasljeđivanje - trait. Može izgledati da je trait nešto kao Java sučelje sa konkretnim metodama. No, trait može imati skoro sve što ima i klasa, npr. može imati i polja, a ne samo metode. Trait se, zapravo, kompajlira u Java sučelje i pripadajuće pomoćne klase koje sadrže implementaciju metoda i atributa.

U Scali, klasa može naslijediti samo jednu (direktnu) nadklasu, ali zato može naslijediti više traitova. Može se reći da klasa koja nasljeđuje drugu klasu i (barem jedan) trait, predstavlja "miksanu" klasu (dok pojam mixin označava trait ili klasu od koje miksana klasa nasljeđuje metode, a ne strukturu). Sljedeći primjer iz [5] prikazuje klasu Frog, koja nasljeđuje (označava se sa extends) klasu Animal (u ovom slučaju je to apstraktna klasa) i nadopunjava se sa dva traita (označava se sa with; kada bi se nasljeđivao samo jedan trait, i nijedna klasa, mogla bi se koristiti riječ extends, umjesto with). Vidi se i da klasa može nadjačati (override) metodu koju je naslijedila iz traita, kao što može onu koju je naslijedila iz klase:

class Animal

trait Philosophical {

def philosophize() { println("I consume memory, therefore I am!") }

}

trait HasLegs

class Frog extends Animal with Philosophical with HasLegs {

override def toString = "green"

override def philosophize() {

println("It ain't easy being "+ toString +"!")

}

}

scala> val phrog: Philosophical = new Frog

phrog: Philosophical = green

scala> phrog.philosophize()

It ain't easy being green!

Jedna od dvije najvažnije upotrebe traita je automatsko dodavanje metoda klasi. Kaže se da traitovi

obogaćuju tanko sučelje (thin interface) klase (ne misli se na Java sučelje), pretvarajući ga u bogato sučelje (rich interface).

Page 8: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

8

Postoje dvije važne razlike između klase i traita. Prvo, trait ne može imati parametre klase (class parameters – ne misli se na generičke parametre), tj. parametre koji se šalju primarnom konstruktoru:

class Point(x: Int, y: Int)

trait NoPoint(x: Int, y: Int) // Does not compile

Druga razlika je da su kod klasa pozivi metode super statički vezani (statically bound), a kod traita su

dinamički vezani, što omogućava da se traitovi koriste za nadograđujuću modifikaciju (stackable modifications) klasa, tj. da se metode klase dinamički nadograđuju metodama iz traita. U sljedećem primjeru iz [5] kreira se apstraktna klasa IntQueue, koju nasljeđuju jedna konkretna klasa BasicIntQueue i dva traita. Trait Incrementing povećava element reda za jedan, a trait Filtering uzima samo one elemente koji su veći ili jednaki nuli:

import scala.collection.mutable.ArrayBuffer

abstract class IntQueue {

def get(): Int

def put(x: Int)

}

class BasicIntQueue extends IntQueue {

private val buf = new ArrayBuffer[Int]

def get() = buf.remove(0)

def put(x: Int) { buf += x }

}

trait Incrementing extends IntQueue {

abstract override def put(x: Int) { super.put(x + 1) }

}

trait Filtering extends IntQueue {

abstract override def put(x: Int) { if (x >= 0) super.put(x) }

}

scala> val queue = (new BasicIntQueue with Incrementing with Filtering)

queue: BasicIntQueue with Incrementing with Filtering...

scala> queue.put(-1); queue.put(0); queue.put(1)

scala> queue.get()

res15: Int = 1

scala> queue.get()

res16: Int = 2

Budući da je prvi element (-1) manji od nule, filtriranje ga je odbacilo, ali je zadržalo elemente 0 i 1,

koji su nakon inkrementiranja dali vrijednost 1 i 2. Te su vrijednosti smještene u red i onda preuzete iz reda. Dakle, redoslijed miksanja je važan. Precizna pravila slijede, ali ukratko se može reći da se prvo pozivaju oni traitovi koji su desnije. Ako oni koriste metodu super, onda pozivaju metodu koja se nalazi u traitu slijeva itd. U konkretnom slučaju prvo se pozvala metoda iz (desnog) traita, tj. napravilo se filtriranje, a onda je ona pozvala metodu iz lijevog traita, tj. napravilo se inkrementiranje elementa.

Trait se može koristiti i selektivno na razini objekta, bez da se veže za (cijelu) klasu. Npr., pretpostavimo da klasa Macka ne nasljeđuje trait Programer. Instanca ipak može naslijediti taj trait:

val jakoPametnaMacka = new Macka("Mica maca") with Programer

Kako kažu autori u [5], iako traitovi omogućavaju nešto slično višestrukom nasljeđivanju u nekim

drugim jezicima, razlika je u barem jednoj važnoj stvari – interpretaciji metode super. Kod višestrukog nasljeđivanja metoda koja se poziva sa super može se odrediti tamo gdje se poziv nalazi. Kod traitova se to, međutim, određuje metodom koja se zove linearizacija (linearization) klase i traitova koji su miksani s tom klasom. Linearizaciju slikovito prikazuje ovaj primjer iz [5]:

class Animal

trait Furry extends Animal

trait HasLegs extends Animal

trait FourLegged extends HasLegs

class Cat extends Animal with Furry with FourLegged

Page 9: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

9

Slika 3.4 prikazuje na tom primjeru hijerarhiju klasa i traitova, te linearizaciju. Nasljeđivanje se prikazuje pomoću tradicionalne UML notacije: strelice sa bijelim, trokutastim vrhovima označavaju nasljeđivanje (strelice pokazuju prema nadtipu). Strelice sa crnim vrhovima označavaju linearizaciju. Te strelice kreću se u smjeru u kojem se dešavaju pozivi super metoda:

Slika 3.4. Hijerarhija nasljeđivanja i linearizacije klase Cat; Izvor: [5]

Linearizacija svake klase ili traita zasebno, prikazana je na sljedećoj slici 3.5:

Slika 3.5. Linearizacija tipova u hijerarhiji klase Cat; Izvor: [5]

Autori u [5] kažu – kad god implementiramo višestruko iskoristivu kolekciju ponašanja (a reusable

collection of behavior), moramo odlučiti da li ćemo koristiti trait ili apstraktnu klasu. Iako nema čvrstih pravila, daju ove smjernice:

- ako se ponašanje neće ponovno koristiti, napravimo konkretnu klasu; - ako će se ponašanje koristiti u više nepovezanih klasa, napravimo trait, jer se samo on može

miksati u različite dijelove hijerarhije klasa; - ako želimo nasljeđivati iz Java koda, napravimo apstraktnu klasu; naime, budući da u Javi ne

postoji nešto analogno traitovima, vrlo je teško u Javi naslijediti trait; nasuprot tome, naslijediti u Javi Scala klasu jednostavno je, kao da je to Java klasa.

3.4. Generičke klase i tipovi

Prije govora o generičkim klasama i tipovima, podsjetimo se da u Scali ponekad možemo izostaviti

eksplicitno navođenje tipa (npr. kod varijable ili povratne vrijednosti funkcije), jer Scala kompajler često može iz programskog konteksta zaključiti kojeg tipa bi trebao biti neki element. Kako je navedeno u 3.1, to je tzv. zaključivanje o tipovima (type inference). Slijedi primjer iz [7]:

var year: Int = 2009 // eksplicitno naveden tip Int

var anotherYear = 2009 // Scala zaključuje da je tip Int

var greet = "Hello there" // ... String

var builder = new StringBuilder("hello") // ... StringBuilder

println(builder.getClass())

Generičke klase imaju parametre koji predstavljaju tipove, tzv. parametre-tipove (type parameters; u

[2] i [3] ih se naziva generičkim parametrima). Kad ne bi postojale generičke klase, mogli bismo reći da su tip i klasa jedno te isto (ako zanemarimo Java primitivne tipove, koji nisu klase). No, generička klasa nije "gotov" tip dok se u njenim parametrima-tipovima generički tipovi ne zamijene konkretnim tipovima. Generičke klase mogu se nasljeđivati.

Page 10: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

10

U sljedećem primjeru iz [6] prikazuje se (apstraktna) generička klasa Stack, koju nasljeđuje (konkretna) generička klasa EmptyStack, pri čemu obje imaju isti generički tip A. Klasa EmptyStack koristi se tako da se generički tip A zamijeni sa konkretnim tipom Int:

abstract class Stack[A] {

def push(x: A): Stack[A] = new NonEmptyStack[A](x, this)

def isEmpty: Boolean

def top: A

def pop: Stack[A]

}

class EmptyStack[A] extends Stack[A] {

def isEmpty = true

def top = error("EmptyStack.top")

def pop = error("EmptyStack.pop")

}

class NonEmptyStack[A] extends Stack[A] {...}

val x = new EmptyStack[Int]

val y = x.push(1).push(2)

println(y.pop.top)

Osim klasa, i metode mogu imati parametre-tipove, kao u sljedećem primjeru iz [6], gdje se funkcija

koristi tako da se generički tip A zamijeni sa konkretnim tipom String: def isPrefix[A](p: Stack[A], s: Stack[A]): Boolean = {

p.isEmpty ||

p.top == s.top && isPrefix[A](p.pop, s.pop)

}

val s1 = new EmptyStack[String].push("abc")

val s2 = new EmptyStack[String].push("abx").push(s1.top)

println(isPrefix[String](s1, s2))

U sljedećim primjerima iz [7] vidjet ćemo kako se generički tipovi u metodama ograničavaju sa

gornjom ili donjom granicom (upper / lower bound). Pretpostavimo da imamo klasu Dog koja nasljeđuje klasu Pet i metodu workWithPets( ) koja prihvaća niz objekata tipa Pet:

class Pet(val name: String) {

override def toString() = name

}

class Dog(override val name: String) extends Pet(name)

def workWithPets(pets: Array[Pet]) {}

Ako sada kreiramo niz objekata klase Dog i pošaljemo ga prethodnoj metodi, dobit ćemo grešku:

val dogs = Array(new Dog("Rover"), new Dog("Comet"))

workWithPets(dogs) // Compilation ERROR

U Scali ne možemo poslati niz objekata tipa Dog metodi koja prihvaća niz objekata tipa Pets (Java bi to kod kompajliranja dozvolila, ali bi puklo kod izvođenja). Ono što trebamo napraviti je da definiramo generički tip (npr. T) i ograničimo ga gornjom granicom. U ovom slučaju gornja granica je tip Pet, pa se ograničavanje označava sa [T <: Pet] (uzgred, u Javi bi se to označilo sa <T extends Pet>, a u Eiffelu sa [T -> Pets]), što znači da konkretan tip mora biti (nepravi) podtip od tipa Pet:

def playWithPets[T <: Pet](pets: Array[T]) =

println("Playing with pets: " + pets.mkString(", " ))

val dogs = Array(new Dog("Rover"), new Dog("Comet"))

playWithPets(dogs) // rezultat je: Playing with pets: Rover, Comet

Page 11: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

11

Ako bismo prethodnoj metodi pokušali poslati niz objekata koji ne pripadaju podtipu od Pet, opet bismo dobili grešku kod kompajliranja. U sljedećem primjeru vidjet ćemo ograničavanje generičkog tipa na donju granicu. U metodi copyPets(), koja kopira izvorišni niz Array[S] u destinacijski niz Array[D], destinacijski generički tip D je morao biti označen kao nadtip izvorišnog generičkog tipa S, sa D >: S:

def copyPets[S, D >: S](fromPets: Array[S], toPets: Array[D]) = { //...}

val pets = new Array[Pet](10)

copyPets(dogs, pets)

U prethodna dva primjera ograničavali smo (pomoću gornje ili donje granice) generički tip u definiciji metode koja koristi kolekciju. Ako bismo bili autori kolekcije, slično ograničavanje bismo mogli raditi nad generičkim tipom u definiciji kolekcije (tj. klase). No, mogli bismo koristiti i tzv. anotaciju varijance (variance annotation). To se radi tako da se u definiciji klase generički tip označi sa prefiksima + ili -, npr. +T ili –T umjesto T, kao u sljedećem primjeru:

class MyList[+T] //...

var list1 = new MyList[Int]

var list2: MyList[Any] = null

list2 = list1 // OK

Stavljajući prefiks + generičkom tipu T, označili smo da taj generički tip ima kovarijantno (covariant)

ponašanje, odnosno rekli smo Scali da je npr. MyList[Int] podtip od MyList[Any], u skladu s tim što je Int podtip od Any (zato se naziva kovarijantno ponašanje). Zato smo mogli pridružiti varijablu tipa MyList[Int] varijabli tipa MyList[Any].

Suprotno od kovarijantnog ponašanja je kontravarijantno (contravariant) ponašanje, kada se generički parametar T označi sa –T, kao u sljedećem primjeru, gdje je MyList[Any] podtip od MyList[Int], što je suprotno ("kontra") tome što je Int podtip od Any (zato se naziva kontravarijantno ponašanje):

class MyQueue[-T] //...

var list1 = new MyQueue[Any]

var list2: MyQueue[Int] = null

list2 = list1 // OK

Podrazumijevano (default) ponašanje u Scali je invarijantno (nonvariant) ponašanje. Vidjeli smo prije da su nizovi u Scali invarijantni, pa smo kod njih morali ograničavati generičke tipove sa gornjom ili donjom granicom, da bismo postigli željeno ponašanje.

Primijetimo još nešto vezano za nasljeđivanje. Za razliku od Eiffela, a slično kao Java, Scala nema mogućnost da tip parametra u metodi podklase (koja je nadjačala metodu iz nadklase) bude kovarijantan u odnosu na tip parametra u nadjačanoj metodi nadklase. No, zahvaljujući riječi override, Scala kompajler takav pokušaj prepoznaje kao grešku, dok bismo kod Jave imali (neželjeno) preopterećenje (overloading) metode. U primjeru, parametar metode eat() ne može u podklasi promijeniti tip iz Food u ChildFood:

class Food(val name: String) {

override def toString() = "Food: " + name

}

class ChildFood(override val name: String, var childMinAge: Int)

extends Food(name) {

override def toString() = "child " + name

}

class Parent(val name: String) {

def eat (pFood: Food) {

println ("In parent procedure: " + name + " eats " + pFood)

}

}

class Child(override val name: String, var childMinAge: Int)

extends Parent(name) {

override def eat (pChildFood: ChildFood) {

println ("In child procedure: " + name + " eats " + pChildFood)

}

}

<console>:11: error: method eat overrides nothing

override def eat (pChildFood: ChildFood) {

Page 12: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

12

4. SCALA KAO FUNKCIJSKI PROGRAMSKI JEZIK Kako je već navedeno u uvodu, prvi funkcijski jezik Lisp (skraćenica od LISt Processing), nastao je

još davne 1958. godine. Funkcijski jezici se od tada neprekidno koriste. Ipak, većina programera koristi se imperativnim programskim jezicima (objektnim ili neobjektnim). No, u zadnjih nekoliko godina se povećao interes za funkcijske jezike. Funkcijski jezici obećavaju bolju modularnost, a modularni programi sastoje se od komponenti koje se mogu razumjeti i koristiti nezavisno od cjeline, pa se lakše spajaju u cjelinu.

Većina nas nije vična funkcijskom programiranju (uključujući autora ovog rada, koji ima neka iskustva sa Prologom, ali to je pak logičko programiranje, i to logičko programiranje u okviru relacijske paradigme, a ne funkcijsko programiranje). Bez obzira na količinu iskustva u imperativnom programiranju, suočavanje sa funkcijskim programiranjem predstavlja izazov, jer traži promjenu načina razmišljanja. Budući da je Scala moćan objektno-orijentirani jezik, koji ima i značajne funkcijske osobine (Scala je jedini statički jezik na JVM-u koji podržava objektno-orijentirano i funkcijsko programiranje), možemo na početku koristiti njegove objektno-orijentirane osobine, te se polako privikavati i na funkcijske. Poslije možemo koristiti onaj stil (imperativni ili funkcijski) koji nam više odgovara za određeni problem.

Kako kažu autori u [5], prvi korak je raspoznavanje razlike (između imperativnog i funkcijskog stila) u

programskom kodu. Ako kod sadrži barem jednu var varijablu, onda je to vjerojatno imperativni stil, a ako sadrži samo val varijable, onda je to vjerojatno funkcijski stil. Ako želimo pisati funkcijskim stilom, trebamo pisati bez var varijabli. To nije lako za nas navikle na imperativno programiranje. Sljedeći primjer iz [5] prikazuje postepenu transformaciju imperativnog koda u kod koji je sve više funkcijski:

def printArgs(args: Array[String]): Unit = {

var i = 0

while (i < args.length) {

println(args(i))

i += 1

}

}

def printArgs(args: Array[String]): Unit = {

for (arg <- args)

println(arg)

}

def printArgs(args: Array[String]): Unit = {

args.foreach(println)

}

Refaktorirana metoda printArgs još uvijek nije čisto funkcijska, jer ima popratni efekt (side-efect) –

štampa na standardni output stream. Znak da funkcija ima popratni efekt je i tip povratne vrijednosti Unit, što znači da funkcija ne vraća ništa interesantno (praktički je to procedura, a procedura se označava i tako da se ne koristi znak jednakosti prije bloka programskog koda). Više funkcijski pristup je da se napravi metoda koja formatira primljene argumente, ali ih ne štampa, kao ova:

def formatArgs(args: Array[String]) = args.mkString("\n")

Funkcija formatArgs nema popratni efekt. Zato se ona može lakše testirati. Ako se ipak želi štampati

rezultat, ona se može pozvati iz druge metode, koja ima popratni efekt: println(formatArgs(args))

Svaki korisni program mora imati neki popratni efekt, inače ne bi mogao slati rezultate vanjskom

svijetu. No, cilj je da se popratni efekti izoliraju u manji broj programskih modula. Preporuka za Scala programere bila bi: preferirajmo val varijable, imutabilne objekte, metode bez popratnih efekata, Ali, to ne znači da ćemo potpuno izbaciti var varijable, mutabilne objekte i popratne efekte – treba ih koristiti kada predstavljaju bolji izbor (naravno, izabrati pravu varijantu nije lako).

Često se postavlja pitanje – koji skup osobina mora imati neki programski jezik da bi se mogao

nazvati funkcijskim programskim jezikom.

Page 13: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

13

Vjerojatno ne postoji jednoznačan odgovor, ali obično se navode ove osobine kao nužne: - funkcije višeg reda (higher-order functions); - leksičko zatvaranje (lexical closure); - podudaranje (sparivanje) uzorka (pattern matching); - jednokratno pridruživanje (single assignment); - lijena evaluacija (lazy evaluation); - zaključivanje o tipovima (type inference); - optimizacija repnog poziva (tail call optimization); - razumijevanje listi (list comprehension): kompaktan i ekspresivan način definiranja listi kao

osnovnih podatkovnih struktura funkcijskog programa; - monadički efekti (monadic effects). Neki dodaju još npr: - funkcije kao vrijednosti, "građani prvog reda" ("first-class value"); - anonimne funkcije; - currying; - sakupljanje smeća (garbage collection). Već smo vidjeli što znači jednokratno pridruživanje (single assignment) – to je kada u Scali koristimo

val varijablu (ili u Javi final varijablu), kojoj se vrijednost može pridružiti samo jednom. Sakupljanje smeća (garbage collection) poteklo je još od Lispa, ali kasnije ga je preuzeo npr. OOPL Smalltalk, pa Eiffel, pa Java ..., tako da to više nije osobina (samo) funkcijskih jezika. Zaključivanje o tipovima (type inference) smo vidjeli u podtočki 3.4. Optimizacija repnog poziva (tail call optimization) je generalizacija optimizacije repne rekurzije (tail recursion), koju smo spomenuli u podtočki 3.1. U nastavku ćemo ukratko proći kroz većinu preostalih navedenih osobina. Neke od njih su samo "sintaksni šećer" (syntactic sugar), tj. olakšavaju rad programeru, ali ne predstavljaju stvarnu dopunu programskom jeziku i ne traže poseban "napor" od kompajlera. Takve su npr. podudaranje uzorka (pattern matching) ili razumijevanje listi (list comprehension). Neke druge osobine predstavljaju stvarnu dopunu programskom jeziku i postavljaju značajne zahtjeve pred kompajler. Ponekad, kao lijena evaluacija (lazy evaluation), traže čak i podršku od strane run-time sustava.

Funkcije su u Scali vrijednosti, "građani prvog reda" ("first-class value”). Kao i svaka druga

vrijednost, mogu biti pridružene nekoj varijabli, poslane kao parametri nekoj drugoj funkciji, ili vraćene kao rezultat neke druge funkcije. Funkcije koje kao parametar ili povratnu vrijednost imaju neku drugu funkciju, zovu se funkcije višeg reda (higher-order functions). Budući da su u Scali funkcije vrijednosti, a istovremeno su u Scali sve vrijednosti objekti, slijedi da su u Scali sve funkcije objekti.

Sljedeći primjer iz [6] prikazuje funkciju višeg reda imena sum, koja kao prvi parametar prima (drugu)

funkciju, što se u funkciji sum označava sa f: Int => Int. Time se kaže da ta druga funkcija (koja se koristi kao parametar), mora preslikavati Int u Int (primijetimo i to da je funkcija sum rekurzivna i da ima repnu rekurziju):

def sum(f: Int => Int, a: Int, b: Int): Int =

if (a > b) 0 else f(a) + sum(f, a + 1, b)

Sada definiramo tri funkcije koje imaju preslikavanje Int => Int i koristimo ih (za punjenje vrijednosti

val varijabli) kao prvi parametar funkcije sum: def id(x: Int): Int = x

def square(x: Int): Int = x * x

def powerOfTwo(x: Int): Int = if (x == 0) 1 else 2 * powerOfTwo(x - 1)

val sumInts = sum(id, 1, 5) // 1+2+3+4+5=15

val sumSquares = sum(square, 1, 5) // 1+4+9+16+25=55

val sumPowersOfTwo = sum(powerOfTwo, 1, 5) // 2+4+8+16+32=62

Često kao parametre koristimo kratke funkcije, i to jednokratno. Tada, umjesto da definiramo

eksplicitne funkcije, možemo direktno kao aktualne parametre koristiti tzv. anonimne funkcije, kao u sljedećem primjeru:

val sumInts2 = sum((x: Int) => x, 1, 5) // = 15

val sumSquares2 = sum((x: Int) => x * x, 1, 5) // = 55

Page 14: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

14

Anonimna funkcija je jedna varijanta funkcijskog literala. Sintaksa funkcijskog literala dana je na sljedećoj slici 4.1:

slika 4.1. Sintaksa funkcijskog literala u Scali; Izvor: [5]

U prethodnim primjerima funkcija sum je, uz prvi parametar – funkciju, imala i dva dodatna

parametra a i b, koji se ne čine naročito zanimljivima. U nastavku se prikazuje kako se funkcija sa n parametara može pretvoriti u funkciju sa manje od n parametara. Takav stil definicije funkcije naziva se currying, po svom promotoru Haskell B. Curryu, logičaru iz 20. stoljeća (po njemu je ime dobio i programski jezik Haskell), iako su ideju još prije dali Gottlob Frege i Moses Schönfinkel.

Slijedi prikaz modificirane funkcije sum, koja sada ima samo jedan parametar, funkciju f, koja preslikava Int u Int. No, funkcija sum kao povratnu vrijednost više nema Int, već (Int, Int) => Int, jer sadrži unutarnju (rekurzivnu) funkciju sumF, koja ima navedenu signaturu i koju funkcija sum vraća kao povratnu vrijednost:

def sum(f: Int => Int): (Int, Int) => Int = {

def sumF(a: Int, b: Int): Int =

if (a > b) 0 else f(a) + sumF(a + 1, b)

sumF

}

Sada možemo definirati funkcije, ili u ovom slučaju varijable (npr. dvije) koje koriste funkciju sum: val sumInts = sum(x => x)

val sumSquares = sum(x => x * x)

pa te dvije varijable možemo koristiti tako da im pošaljemo vrijednosti za parametre a i b:

sumInts(1, 5) + sumSquares(1, 5) // 15 + 55 = 70

Kako se računanje izvršavalo? Scala je pretvorila prethodni izraz u sljedeći: sum (x => x) (1, 5) + sum(x => x * x)(1, 5)

Izraz f (args1) (args2) (args3) je ekvivalentan izrazu ((f (args1)) (args2)) (args3), tj. aplikacija funkcije

radi se s lijeva na desno. Zapravo, u Scali se definicija funkcije u "currying stilu" može napisati još jednostavnije, bez unutarnje

funkcije. Npr. prethodna verzija funkcije sum može se napisati i ovako: def sum(f: Int => Int)(a: Int, b: Int): Int =

if (a > b) 0 else f(a) + sum(f)(a + 1, b)

No, sada ne možemo definirati varijable (ili funkcije) koje koriste funkciju sum kao što smo to radili

prije, već moramo odmah navesti sve parametre. Međutim, parametre možemo navesti, ali ih ostaviti nevezane (unbound), što radimo tako da umjesto konkretnog parametra stavimo znak _, pa na taj način dobijemo parcijalno apliciranu funkciju (partially applied function). Npr. parametri a i b su ovdje nevezani:

val sumInts = sum(x => x) // kompajler ne propušta; nedostaju parametri

val sumInts = sum(x => x) (_, _) // ispravno; nevezani parametri a i b

val sumSquares = sum(x => x * x) (_, _)

sumInts(1, 5) + sumSquares(1, 5) // 15 + 55 = 70; isto kao prije

Page 15: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

15

Prikažimo kako možemo koristiti blok koda koji ima varijable koje nisu lokalne varijable, a nisu ni povezane sa parametrima tog bloka koda. Prije nego pozovemo taj blok koda, morat ćemo vezati te nevezane varijable. No, moći ćemo ih vezati i sa varijablama izvan lokalnog dosega (scope), tj. moći ćemo ih vezati sa varijablama iz okruženja. To se zove leksičko zatvaranje (lexical closure), ili samo zatvaranje (closure).

U sljedećem primjeru iz [6], funkcija loopThrough() prolazi kroz sve elemente od 1 do zadanog broja, koji je njen prvi parametar. Drugi parametar je blok programskog koda, koji se u funkciji poziva za svaku vrijednost od 1 do zadanog broja:

def loopThrough(number: Int)(closure: Int => Unit) {

for (i <- 1 to number) { closure(i) }

}

Definirajmo blok koda koji ćemo poslije poslati prethodnoj metodi i pridružimo ga varijabli addIt: var result = 0 val addIt = { value: Int => result += value } Primijetimo da je unutar bloka koda varijabla value vezana za parametar, dok varijabla result nije niti

lokalna varijabla, niti vezana za parametar - ona je vezana za varijablu result izvan bloka koda. Sada možemo koristiti blok koda (odnosno varijablu addIt kojoj je on pridružen) u funkciji loopThrougth():

loopThrough(5) { addIt }

println("Total of values from 1 to 5 is " + result)

Nakon tog poziva, varijabla result (u glavnome kodu) bit će promijenjena (na vrijednost 15). Funkcijski jezici poznati su po tome da imaju izvrstan način rada sa kolekcijama, naročito sa listama

(list). Glavne Scala kolekcije su List, Set i Map. List je uređena kolekcija objekata, Set je neuređena kolekcija objekata, a Map je skup parova (ključ, vrijednost). Set i Map kolekcije imaju dvije varijante - imutabilnu i mutabilnu, što za kolekciju Set prikazuje sljedeća slika 4.2:

Slika 4.2. U Scali većina kolekcija (izuzeci su List, Array)

ima mutabilnu i imutabilnu varijantu (na slici je Set); Izvor: [5]

List kolekcija ima samo imutabilnu varijantu, a Array samo mutabilnu. Željena verzija kolekcije bira se iz odgovarajućih paketa scala.collection.mutable ili scala.collection.immutable. Ako želimo modificirati kolekciju tako da su sve operacije unutar jedne dretve, možemo izabrati mutabilnu kolekciju. No, ako kolekciju želimo koristiti između više dretvi ili aktora (actors; bit će prikazani u sljedećoj točki) imutabilne kolekcije su svakako sigurnije. Podrazumijevane (default) kolekcije su imutabilne, tj. treba eksplicitno reći ako želimo koristiti mutabilne kolekcije.

U nastavku prikazujemo samo neke osnovne primjere rada sa Scala Set i List kolekcijama. Mogućnosti rada sa kolekcijama su brojne. Započinjemo sa Set kolekcijama.

Page 16: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

16

var colors1 = Set("Blue", "Green", "Red")

var colors2 = colors1

println(colors2) // prikazuju se sve 3 boje kao u skupu colors1

colors1 += "Black" // skupu colors1 dodaje se crna boja

println(colors1) // prikazuju se 4 boje

println(colors2) // prikazuju se 3 boje, skup colors2 nije se promijenio

Primijetimo da nismo trebali pisati riječ new za kreiranje skupa. Scala radi sljedeću pretvorbu

naredbe:

val colors1 = Set("Blue", "Green", "Red") // izvorno

val colors1 = // pretvoreno

new scala.collection.immutable.Set3[String]("Blue", "Green", "Red")

Nad skupovima se može npr. raditi unija ili presjek

val unijaBoja = colors1 union colors2

val presjekBoja = colors1 intersect colors2

Slijedi par primjera iz [6] sa List kolekcijama (slično se može raditi i sa drugim kolekcijama): val feeds = List("blog.toolshed.com",

"pragdave.pragprog.com", "dimsumthinking.com/blog")

println("First feed: " + feeds.head) // First feed: blog.toolshed.com

println("Second feed: " + feeds(1))

// Second feed: pragdave.pragprog.com; prvi element označava se sa (0)!

println(feeds.filter(_ contains "blog").mkString(", "))

// blog.toolshed.com, dimsumthinking.com/blog

println(feeds.forall(_ contains "com")) // true

println(feeds.forall(_ contains "dave")) // false

println(feeds.exists(_ contains "dave")) // true

println(feeds.exists(_ contains "bill")) // false

val prijatelji = List("Ana", "Pero", "Marica", "Ivo")

prijatelji foreach

{ prijatelj => println(prijatelj + " je dobar prijatelj") }

// Ana je dobar prijatelj

// Pero je dobar prijatelj ... itd.

Podudaranje (sparivanje) uzorka (pattern matching) je vrlo važna mogućnost u Scali, iako spada u "sintaksni šećer". Naročito se koristi kod aktora (actors), ali i za parsiranje i sl. Npr. podudaranje uzorka se u Scali koristi i kod obrade iznimaka (exception), gdje catch dio try - catch - finally bloka radi drugačije nego u Javi (uz to što ne mora imati checked exceptions). Slijedi nekoliko jednostavnih primjera iz [7]. Prvo radimo sa konstantama, te radimo iscrpnu obradu svih mogućnosti:

def activity(day: String) {

day match {

case "Sunday" => print("Eat, sleep, repeat... " )

case "Monday" => print("...code for fun..." )

// ...

case "Saturday" => print("Hangout with friends... " )

}

}

U sljedećem primjeru radimo sa objektima, ali ne radimo iscrpnu obradu svih mogućnosti. Da se ne bi desila iznimka (exception) MatchError, koristimo višeznačnik (wildcard) _, kojim obuhvaćamo sve mogućnosti koje nisu eksplicitno obrađene:

object DayOfWeek extends Enumeration {

val SUNDAY = Value("Sunday")

val MONDAY = Value("Monday" ) // ... itd.

}

Page 17: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

17

def activity(day: DayOfWeek.Value) {

day match {

case DayOfWeek.SUNDAY => println("Eat, sleep, repeat..." )

case DayOfWeek.SATURDAY => println("Hangout with friends" )

case _ => println("...code for fun..." )

}

}

Osim konstanti i istovrsnih objekata, u case možemo imati i n-torke (tuples) i liste:

def processCoordinates(input: Any) {

input match {

case (a, b) => printf("Processing (%d, %d)... " , a, b)

case "done" => println("done" )

case _ => null

}

}

processCoordinates((39, -104))

processCoordinates("done" )

Često u case imamo i objekte različitih tipova. Ponekad želimo da objekte različitih tipova ili karakteristika obradimo drugačije, pa možemo koristiti provjeru na tip, ali i dodatne if uvjete. Naglasimo da redoslijed obrade ide od gore prema dolje, pa se u sljedećem slučaju prvo obrađuju poruke (msg) tipa Int duže od 1000000, pa ostale poruke tipa Int, pa poruke tipa String:

def process(input: Any) {

input match {

case (a: Int, b: Int) => print("Processing (int, int)... " )

case (a: Double, b: Double) =>

print("Processing (double, double)... " )

case msg: Int if (msg > 1000000) =>

println("Processing int > 1000000" )

case msg: Int => print("Processing int... " )

case msg: String => println("Processing string... " )

case _ => printf("Can't handle %s... " , input)

}

}

Lijena evaluacija (lazy evaluation) ili lijene vrijednosti su vrlo važna osobina funkcijskih jezika, te ne

predstavljaju samo "sintaksni šećer". Riječ je o tome da se inicijalizacija vrijednosti odgađa do trenutka kada se (lijena) vrijednost prvi put koristi. To može biti značajno i zato što se neka vrijednost možda neće koristiti, a njeno računanje je zahtjevno, pa lijena evaluacija može imati bolje performanse. Lijena evaluacija koristi se i kod generiranja beskonačnih nizova – ona redom generira sljedeći član niza.

U sljedećem primjeru iz [6] se za objekt klase Employee iz baze podataka čita redak koji predstavlja nadređenog radnika, te čitava lista redaka koji predstavljaju tim s kojim radnik radi. Svakako je brže da se baza podataka ne treba čitati dok to nije potrebno. Prva varijanta je bez lijene evaluacije, a druga s njom:

// bez lijene evaluacije

class Employee

(id: Int, name: String, managerId: Int)

{

val manager: Employee = Db.get(managerId)

val team: List[Employee] = Db.team(id)

}

// sa lijenom evaluacijom

class Employee

(id: Int, name: String, managerId: Int)

{

lazy val manager: Employee = Db.get(managerId)

lazy val team: List[Employee] = Db.team(id)

}

Page 18: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

18

5. SCALA I KONKURENTNO PROGRAMIRANJE U ovoj točki prikazat ćemo ukratko tri "stila" konkurentnog programiranja u Scali. Prvi stil bit će

uobičajeni imperativan stil, sličan onome u Javi. Drugi stil temelji se na aktorima (actors). Scala podržava aktore kroz barem dvije biblioteke. Jedna je standardna Scala biblioteka za aktore, a druga je temeljena na Akka frameworku (pisan u Scali). Akka aktori se danas sve više koriste, jer su bogatiji nego standardni Scala aktori. Zapravo, od verzije Scala 2.10 neke Akka biblioteke postale su standardne Scala biblioteke, a "stari" Scala aktori su postali zastarjeli. Akka framework osim aktora ima i puno drugih dodataka. Jedan od njih je i softverska transakcijska memorija, koja će ukratko biti prikazana kao treći stil konkurentnog programiranja.

Kao i u Javi, i u Scali se svaka instanca klase AnyRef (= Java klasa Object) može koristiti kao monitor (uz sve mane koje je naveo Brinch Hansen u [1]), pozivajući operacije navedene u nastavku, koje se ponašaju isto kao u Javi (synchronized je Java ključna riječ, ostalo su metode Java klase Object). U Scali su to metode Scala klase Monitor, koja je primitivna (temeljena na run-time sustavu):

def synchronized[A] (e: => A): A

def wait()

def wait(msec: Long)

def notify()

def notifyAll()

Future je vrijednost koja se računa paralelno sa nekom drugom klijentskom dretvom, kako bi se u

nekom trenutku iskoristila u tom klijentskom programu. Slijedi tipični primjer korištenja (iz [6]) i definicija future metode iz biblioteke scala.concurrent.ops:

import scala.concurrent.ops._

...

val x = future(someLengthyComputation)

anotherLengthyComputation

val y = f(x()) + g(x())

def future[A](p: => A): Unit => A = {

val result = new SyncVar[A]

fork { result.set(p) }

(() => result.get)

}

Scala primjer za čitatelje / pisce (više konkurentnih čitatelja, samo jedan pisac): import scala.concurrent._

class ReadersWriters {

val m = new MailBox

private case class Writers(n: Int), Readers(n: Int) { m send this }

Writers(0); Readers(0)

def startRead = m receive {

case Writers(n) if n == 0 => m receive {

case Readers(n) => Writers(0); Readers(n + 1)

}

}

def startWrite = m receive {

case Writers(n) =>

Writers(n + 1)

m receive { case Readers(n) if n == 0 => }

}

def endRead = m receive {

case Readers(n) => Readers(n1)

}

def endWrite = m receive {

case Writers(n) => Writers(n1);

if (n == 0) Readers(0)

}

}

Page 19: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

19

Slijedi kratak prikaz neimperativnog stila, pomoću poštanskih pretinaca (mailboxes) i aktora (actors). Već neko vrijeme se sve više zagovara konkurentnost temeljena na slanju poruka (message-passing concurrency). Konkurentnost na temelju slanja poruka je prirodni stil za distribuirane sustave i omogućava visoku raspoloživost. To je programski stil kod kojega se program sastoji od nezavisnih entiteta, aktora (actors), koji si šalju poruke asinkrono, bez čekanja na odgovor. Model aktora kreirao je 70-ih Carl Hewitt. Najpoznatiji jezik koji je primijenio taj model (još 80-ih) je Erlang, od kojeg je Scala dosta toga preuzela i nadogradila (za razliku od Scale, Erlang je jezik sa dinamičkom provjerom tipova).

Poštanski pretinci su fleksibilan konstrukt visoke razine, te služe za procesiranje sinkronizacije i komunikacije. Omogućavaju slanje i primanje poruka, a poruka je bilo koji objekt. Postoji posebna poruka TIMEOUT koja se koristi za signaliziranje time-outa:

case object TIMEOUT

Poštanski pretinci implementiraju ovu signaturu: class MailBox {

def send(msg: Any)

def receive[A](f: PartialFunction[Any, A]): A

def receiveWithin[A](msec: Long)(f: PartialFunction[Any, A]): A

}

Poruke se dodaju u poštanski pretinac asinkrono, pomoću send metode (pošiljatelj poruke ne čeka

nakon što pozove send). Poruke se redom preuzimaju iz pretinca pomoću receive (ili receiveWithin) metode, kojoj se šalje procesor poruka f (parcijalna funkcija) kao parametar. Tipično se ta funkcija f implementira pomoću podudaranja uzorka (pattern matching). Receive metoda blokira dok se u pretincu ne pojavi odgovarajuća poruka (dok metoda receive "beskonačno" čeka na poruku, metoda receiveWithin čeka zadano vrijeme - zato se preporučuje njeno korištenje). Zatim se poruka (koja ne mora biti zadnja) vadi iz pretinca i blokirana dretva se restarta primjenjujući procesor f na poruku.

Jednostavan primjer korištenja pretinca je sljedeći (iz [6]): class OnePlaceBuffer {

private val m = new MailBox // An internal mailbox

private case class Empty, Full(x: Int) // Types of messages we deal with

m send Empty // Initialization

def write(x: Int)

{ m receive { case Empty => m send Full(x) } }

def read: Int =

m receive { case Full(x) => m send Empty; x }

}

Slijedi prikaz vrlo pojednostavljene implementacije aktora (iz [6]). U najjednostavnijoj varijanti, aktor

bi se mogao definirati kao miksana kompozicija standardne Java klase Thread i Scala traita MailBox, u kojoj se nadjača run metoda Thread klase, tako da izvršava ponašanje definirano u act metodi. Metoda (neobičnog) imena ! jednostavno poziva send metodu iz klase MailBox:

abstract class Actor extends Thread with MailBox {

def act(): Unit

override def run(): Unit = act()

def !(msg: Any) = send(msg)

}

Prava implementacija aktora je svakako složenija od ove. Prije svega, već u standardnoj Scala biblioteci aktora imamo još jednu varijantu, koja se ne zasniva na tome da svaki aktor bude jedna dretva, već aktori dijele dretve iz pričuve dretvi (thread poll). Umjesto metoda receive i receiveWithin, u toj varijanti postoje ekvivalentne metode react i reactWithin. Razlog postojanja ove druge varijante je taj što su na JVM-u dretve "skupe". Dok se u prvoj varijanti (jedan aktor = 1 dretva) može kreirati tisuće aktora, u drugoj varijanti (aktori dijele dretve iz pričuve) može se bez problema kreirati na stotine tisuća aktora – to su tzv. lagani aktori. No, lagani aktori nemaju samo pozitivne osobine – sa njima je nešto teže raditi. Ako se obrađuju poruke koje nisu jednostavne i kratke, i ako broj aktora koje treba kreirati nije prevelik, onda je bolje koristiti prvu varijantu, tj. ne-lagane aktore i metode receive i receiveWithin.

Page 20: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

20

Sljedeće slike prikazuju kako aktori komuniciraju i kakav je životni ciklus aktora (neovisno da li je implementiran kao lagani ili ne-lagani aktor):

Slika 5.1. Aktori izoliraju mutabilno stanje i komuniciraju slanjem imutabilnih poruka;

Izvor: [8]

Slika 5.2. Životni ciklus aktora; Izvor: [8]

Sljedeći primjer iz [7] prikazuje korištenje ne-laganih aktora pomoću receive metode: import scala.actors._

import scala.actors.Actor._

val caller = self

val accumulator = actor {

var sum = 0

var continue = true

while (continue) {

sum += receive {

case number: Int => number

case "quit" =>

continue = false

0

}

}

caller ! sum

}

accumulator ! 1

accumulator ! 7

accumulator ! 8

accumulator ! "quit"

receive { case result => println("Total is " + result) }

Total is 16

Glavni program šalje aktoru (koji je vezan za val varijablu accumulator) redom poruke 1, 7, 8, "quit",

ne čekajući na odgovor, dok ne dođe do (svoje) metode receive, kada čeka odgovor. Aktor se vrti u petlji dok ne dobije poruku "quit", nakon čega šalje rezultat glavnom programu. Aktor u svojoj receive metodi povećava rezultat za dobiveni broj (kad dobije poruku "quit", povećava za nulu).

Page 21: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

21

Druga podvarijanta prikazuje korištenje ne-laganih aktora pomoću receiveWithin metode: val caller = self

val accumulator = actor {

var sum = 0

var continue = true

while (continue) {

sum += receiveWithin(10000) {

case number: Int => number

case TIMEOUT =>

println("Timed out! Will return result now")

continue = false

0

}

}

caller ! sum

}

accumulator ! 1

accumulator ! 7

accumulator ! 8

receiveWithin(20000) { case result => println("Total is " + result) }

Timed out! Will return result now

Total is 16

Vidi se da i aktor i glavni program imaju ograničeno čekanje. Aktor čeka maksimalno 10 sekundi, a

glavni program 20 sekundi. Aktor ima obradu TIMEOUT iznimke, tako da vraća rezultat nakon isteka njegovog vremena (10 sekundi), pa ovdje glavni program ne šalje aktoru poruku za kraj.

Sljedeći primjer prikazuje varijantu sa laganim aktorima, gdje aktor koristi reactWithin metodu

(korištenje metode react nije prikazano). Za razliku od receive metode kod ne-laganih dretvi, react metoda ne vraća nikakav rezultat. Može se pojednostavljeno zamisliti da ona interno izvrši iznimku (exception) i vraća dretvu u pričuvu dretvi. Ako želimo nastaviti sa procesiranjem nakon što se sa react obradi jedna poruka, ta poruka mora pozvati neku drugu poruku, ili samu sebe, kao u ovom primjeru:

val caller = self

def accumulate(sum: Int) {

reactWithin(10000) {

case number: Int => accumulate(sum + number)

case TIMEOUT =>

println("Timed out! Will send result now")

caller ! sum

}

println("This will not be called...")

}

val accumulator = actor { accumulate(0) }

accumulator ! 1

accumulator ! 7

accumulator ! 8

receiveWithin(20000) { case result => println("Total is " + result) }

Timed out! Will send result now

Total is 16

Dakle, kod aktora iz standardne Scala biblioteke, lakše je raditi sa ne-laganim aktorima (koji koriste metodu receive), nego sa laganim aktorima (koji koriste metodu react).

Page 22: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

22

U [5] autori daju preporuke o tome kako bi trebalo pisati programe sa aktorima. Prije svega, navode da aktor ne bi smio blokirati, jer ako aktor blokira na nekom zahtjevu, neće uopće niti vidjeti sljedeći zahtjev. U najgorem slučaju može se desiti čak i deadlock (kao što se može desiti kod sinkronizacije pomoću lokota), kad više blokiranih aktora čekaju jedan na drugoga. Aktori bi trebali komunicirati isključivo preko poruka, kako bi omogućili da pisanje višedretvenih programa bude svedeno na pisanje skupa nezavisnih jednodretvenih programa, koji jedan s drugim komuniciraju pomoću asinkronih poruka. Aktori nam omogućavaju da se ne moramo uvijek odreći mutabilnih objekata. Budući da su dobro pisani aktori nezavisni, onda nije važno da li su unutar njih korišteni mutabilni objekti. Međutim, krucijalno je da poruke budu imutabilne.

Za razliku od običnih sinkronih metoda, kod kojih metoda-pozivatelj (druge metode) zna što je radila prije nego je pozvala drugu metodu, aktor-pozivatelj nastavlja sa radom, jer je poziv (drugog) aktora asinkron. Kada dođe odgovor, aktor-pozivatelj "teže interpretira" odgovor drugog aktora. Zbog toga se često u poruke dodaju redundantni podaci.

U prethodnim primjerima rad sa aktorima bio je temeljen na standardnoj Scala biblioteci (do verzije 2.10). Kako je već prije napomenuto, puno bogatiji rad sa aktorima ima framework Akka. Taj framework zajedno sa jezikom Scala i Web frameworkom Play čini tzv. Typesafe Stack, koji se može koristiti iz Scale, iz Jave, ili iz bilo kojeg jezika na JVM-u. Kao i Scala jezik, tako su i Akka i Play open-source programi, i mogu se besplatno preuzeti sa stranica firme Typesafe: typesafe.com. Na stranicama piše (i) sljedeće: "Typesafe was founded in 2011 by the creators of the Scala programming language and Akka middleware, who joined forces to create a modern software platform for the era of multicore hardware and cloud computing workloads."

Ovdje neće biti prikazan rad sa Akka aktorima, već rad sa Akka softverskom transakcijskom memorijom, Akka STM. Napomenimo da se može istovremeno koristiti oboje, i aktori i STM.

Scala transakcije se u Akka STM-u definiraju vrlo jednostavno. Transakcija se stavlja unutar bloka koji počinje rječju atomic (to je različito od transakcija u npr. Oracle bazi podataka, gdje je sve unutar transakcije, jer nova transakcija automatski počinje čim završi stara, tj. nakon COMMIT ili ROLLBACK):

atomic {

//code to run in a transaction....

/* return */ resultObject

}

Akka STM dozvoljava da se transakcije gnijezde, kao u ovom primjeru iz [8]. Prvo se definira klasa

Account, sa atomarnim metodama deposit i withdraw: import akka.stm.Ref

import akka.stm.atomic

class Account(val initialBalance: Int) {

val balance = Ref(initialBalance)

def getBalance() = balance.get()

def deposit(amount: Int) = {

atomic {

println("Deposit " + amount)

if(amount > 0)

balance.swap(balance.get() + amount)

else

throw new AccountOperationFailedException()

}

}

def withdraw(amount: Int) = {

atomic {

val currentBalance = balance.get()

if(amount > 0 && currentBalance >= amount)

balance.swap(currentBalance - amount)

else

throw new AccountOperationFailedException()

}

}

}

Page 23: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

23

Zatim se objekti klase Account koriste u klasi (zapravo singleton klasi, tj. Scala object strukturi) AccountService. Vidi se da se u atomarnoj metodi transfer pozivaju atomarne metode deposit i withdraw, tj. te se transakcije gnijezde unutar transakcije koju čini metoda transfer. Naredbe println služe samo da se lakše prate zbivanja, a naredba sleep unutar metode transfer, kao i kreiranje (drugog) aktora u metodi main, služe da bi se simulirao višekorisnički rad:

object AccountService {

def transfer(from: Account, to: Account, amount: Int) = {

atomic {

println("Attempting transfer...")

to.deposit(amount)

println("Simulating a delay in transfer...")

Thread.sleep(5000)

println("Uncommitted balance after deposit $" + to.getBalance())

from.withdraw(amount)

}

}

def transferAndPrintBalance(

from: Account, to: Account, amount: Int) = {

var result = "Pass"

try {

AccountService.transfer(from, to, amount)

} catch {

case ex => result = "Fail"

}

println("Result of transfer is " + result)

println("From account has $" + from.getBalance())

println("To account has $" + to.getBalance())

}

def main(args: Array[String]) = {

val account1 = new Account(2000)

val account2 = new Account(100)

actor {

Thread.sleep(1000)

account2.deposit(20)

}

transferAndPrintBalance(account1, account2, 500)

println("Making large transfer...")

transferAndPrintBalance(account1, account2, 5000)

}

}

Kako kaže autor u [8], STM ima puno dobrih strana: - omogućava maksimalnu konkurentnost, direktno upravljanu sa stvarnim potrebama aplikacije, a

ne tehničkim stvarima, kao što su zaključavanje i sl.;

- predstavlja programski model bez lokota (lock free); ne moramo voditi brigu o redoslijedu zaključavanja, a ipak nema deadlocka;

- osigurava da se identiteti mijenjaju samo unutar transakcije.

Uza sve prednosti, STM nije bez mana. STM je pogodan za konkurentno čitanje, ali sa relativno

malo konkurentnog mijenjanja. Kada se dvije transakcije sukobe oko mijenjanja istog objekta ili podataka, samo jedna od njih će uspjeti, a druga će se automatski poništiti. Performanse neće biti loše ako su sukobi transakcija koje mijenjaju isti objekt rijetki. Ali, kako se sukobi povećavaju, stvari će u najboljem slučaju biti spore. U najgorem slučaju, desit će se da nijedna transakcija ne napreduje – livelock.

U slučaju puno kolizija kod pisanja, STM nije dobra solucija. Tada je bolje koristiti aktore.

Page 24: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

24

6. ZAKLJUČAK U zadnjih (otprilike) desetak godina sve se više govori (i) o tome da bi programiranje trebalo biti

multiparadigmatsko, tj. da bismo trebali koristiti onu jezičnu paradigmu koja je najprirodnija za rješavanje određenog problema, a ne koristiti "jedan alat za sve probleme". Ovdje postoje barem dva pristupa – jedan je da koristimo više programskih jezika (npr. jedan objektni, jedan funkcijski, jedan logički ...), a drugi pristup kaže da je puno jednostavnije i bolje imati jedan programski jezik koji podržava različite paradigme. Taj drugi pristup podržava i programski jezik Scala, koji je prije svega objektni programski jezik, ali tako nadograđen da ima i značajne funkcijske osobine.

Kreator Scale, profesor Martin Odersky, naglasio je da nije želio da Scala bude 100% kompatibilna sa Javom pod svaku cijenu. Npr. odustao je od toga da Scala nizovi (arrays) budu kovarijantni (covariant) kao u Javi (jedna od najvećih grešaka kod razvoja Jave). Scala je kreirana kao čisti objektno-orijentirani jezik (kao i Eiffel, dok Java nije čisti OOPL), i u nju su uvedene neke objektno-orijentirane mogućnosti koje Java nema. Osim toga, na temelju objektno-orijentiranih mogućnosti izgrađene su i brojne funkcijske mogućnosti, tako da je Scala i funkcijski jezik (ali nije čisti). Objektno-orijentirani pristup dobar je kada se skup klasa prirodno proširuje. Funkcijski pristup pogodniji je kada je skup struktura relativno fiksan, ali se žele uvesti nove operacije nad postojećim strukturama, gdje je najbolji pristup podudaranje uzorka (pattern matching).

Sa funkcijskim osobinama došle su i neke osobine koje su vrlo pogodne za konkurentno programiranje. Scala podržava tri "stila" konkurentnog programiranja. Prvi stil je uobičajeni imperativan stil, sličan onome u Javi. On uključuje i lokote i neblokirajući sinkronizaciju. Drugi stil temelji se na aktorima (actors), koji se do sada nije značajno koristio, osim u jeziku Erlang, od kojega je Scala puno preuzela (i nadogradila). Scala podržava aktore kroz barem dvije biblioteke. Jedna je dosadašnja standardna Scala biblioteka za aktore (do verzije 2.10), a druga je temeljena na Akka frameworku (pisan je u Scali), koji je od Scala verzije 2.10 postao standard (a "stari" Scala aktori se postali zastarjeli). Akka framework osim aktora ima i puno drugih dodataka. Jedan od njih je i softverska transakcijska memorija, treći stil konkurentnog programiranja. Moguće je istovremeno koristiti i aktore i STM.

Scala je izvrstan jezik i za pisanje DSL-ova (Domain-Specific Language), programskih jezika za specifičnu problemsku domenu. Profesor Odersky je krajem 2010. od European Research Councila (ERC) dobio Advanced Investigator Grant (oko 2,5 milijuna eura za projekt u trajanju od 5 godina, 2011.-2016.) za svoj projekt vezan za DSL-ove za paralelno programiranje pisane u Scali. Scala je, kao i Eiffel (kreiran 1986., skoro deset godina prije Jave) vrlo rigorozno matematički specificirana, za razliku od Jave. Scala podržava statičku provjeru tipova, kao i C++, Eiffel, Haskell, Java, C# ... , za razliku od npr. jezika Python, Ruby, Groovy, Clojure ... , kod kojih se greška u tipovima pokaže tek kod izvođenja programa.

Držimo da su u konkurenciji brojnih programskih jezika na JVM-u, velike šanse na strani jezika Scala. Uostalom, to pokazuje i sve veći broj značajnih firmi koje su većim ili manjim dijelom prešle na programiranje u Scali (npr. Twitter, Linkedin, Juniper, Foursquare). No, važno je da se može programirati u Scali bez napuštanja Jave, jer se Java i Scala programski kod mogu jako dobro upotpunjavati.

Page 25: ŠTO POSLIJE PASCALA? PA … SCALA!programiranje na JVM-u, ali i vrlo dobar jezik za moderno obučavanje osnova programiranja. U radu se prikazuju neke objektne osobine, funkcijske

25

LITERATURA 1. Brinch Hansen, P. (1998): Java insecure Parallelism, članak, Syracuse University,

http://brinch-hansen.net/papers/1999b.pdf (svibanj 2013.)

2. Meyer, B. (1997): Object-Oriented Software Construction, Prentice Hall

3. Meyer, B. (2009): Touch of Class - Learning to Program Well with Objects and Contracts, Springer

4. Odersky, M. (2009), The Origins of Scala + The Goals of Scala's Design + The Purpose of Scala's Type System + The Point of Pattern Matching in Scala, (4 intervjua), http://www.artima.com/scalazine/articles/origins_of_scala.html (svibanj 2013.)

5. Odersky, M., Spoon, L., Venners, B. (2010): Programming in Scala (2.izdanje), Artima Press, Walnut Creek, California

6. Odersky, M. (2011): Scala By Example (draft), Programming Methods Laboratory EPFL, Switzerland

7. Subramaniam, V. (2008): Programming Scala - Tackle Multicore Complexity on the JVM, The Pragmatic Bookshelf, Dallas, Texas / Raleigh, North Carolina

8. Subramaniam, V. (2011): Programming Concurrency on the JVM - Mastering Synchronization,

STM, and Actors, The Pragmatic Bookshelf, Dallas, Texas / Raleigh, North Carolina

Zlatko Sirotić, univ.spec.inf. Istra informatički inženjering d.o.o., Pula e-mail: [email protected] Autor radi skoro 30 godina na informatičkim poslovima, uglavnom u poduzeću Istra informatički inženjering d.o.o., Pula. Oracle softverske alate (baza, Designer CASE, Forms 4GL, Reports, JDeveloper IDE, Java) koristi više od 15 godina. Objavljivao je stručne radove na kongresima/konferencijama HrOUG, CASE, KOM, "Hotelska kuća", u časopisima "Mreža", "InfoTrend" i "Ugostiteljstvo i turizam", a neka njegova programska rješanja objavljivana su na web stranicama firmi Quest i Oracle.