Țin minte momentul distinct în 2014, anterior să citesc cartea "The Well Grounded Java Developer". Făcusem patru ani și jumătate de Java în mod profesional și după lucrul pe sisteme mari, entuziasmul care-l aveam pentru programare începea să scadă. Mă întrebam dacă așa o să îmi fie cariera: să înot în if-uri, for-uri, clase și metode?
oare asta-i tot? cod legacy scris la cel mai mic numitor comun, cu tehnologia programării structurate din anii '70?
Asta e tot ce este în programarea profesională, un șir nesfârșit de agonie și plictiseală?
Mi-a fost greu să accept că după șase ani de programare înainte de facultate (doi de joacă înainte de liceu în Game Maker și patru de educație de profil) și încă trei în timpul ei, după ce trecusem prin C++, Delphi, Java, C#, să ajung la capăt și asta să fie tot.
În același moment am început să înclin mai mult spre Python, care era plăcut de folosit și care-mi aducea o anumită reconciliere în plăcerea de a programa prin modul succint de exprimare și a lipsei de restricții.
Cartea și comunitatea de colegi în care eram mi-au deschis însă ochii spre noi posibilități. Java 7 era în vogă, iar pe JVM, platforma pe care lucram avea și alte limbaje ?! Sigur, știam în teorie că ar fi posibil, dar cartea respectivă zicea că e chiar recomandat să folosești și alte limbaje în același proiect ?! Conceptul de programare poliglotă m-a încântat. În sfârșit, aveam un scop. Colegii folosiseră deja limbaje exotice ca F# pe platforma .NET. Era un freamăt legat de Clojure, Groovy și într-o mai mică măsură Scala.
Când am citit cartea, deși era descris în cele mai simple aspecte, Scala mi s-a părut ceva science fiction, l-am notat în minte pentru mai târziu.
Un an mai încolo, am scris un framework de testare în Groovy fiind în acel moment convins de beneficiile limbajelor dinamice. Un an după asta, m-am lămurit că aceasta nu ar fi o soluție viabilă pentru proiecte serioase.
Când s-a ivit oportunitatea de a începe un proiect nou, am insistat să fie în Scala. (Prin niște circumstanțe miraculoase a fost aprobat.)
Au urmat trei ani de lucru în Scala cu niște colegi excepționali, care mi-a reaprins plăcerea de a programa. Deși echipa nu mai există, majoritatea au continuat să lucreze în Scala în alte organizații.
Dacă vă confruntați cu aceeași problemă și simțiți nevoia de o schimbare, citiți în continuare !
Scala s-a născut din ideea că programarea orientată obiect (OOP) și programarea funcțională (FP) nu sunt mutual exclusive și pot fi îmbinate armonios în același limbaj.
Și din dorința de a face un limbaj potrivit pentru a fi folosit atât în scripturi mici cât și pentru sisteme mari. (De aici numele de Scala, un limbaj scalabil.)
Scala 1.0 a fost doar un limbaj de nișă, academic, cu o durată de viață scurtă. Folosit din 2003 de către creatorul Martin Odersky la EPFL. E lansat în 2004 pe JDK 1.4.
Scala 2.0 se lansează în 2006 cu funcționalități și proprietăți extraordinare pentru perioada respectivă. Este adoptat de către o mica elită de programatori, iar în 2008 Twitter anunță că îl folosește în producție, fapt care generează entuziasm și o mai mare creștere a utilizatorilor. Prima conferință mare, Scala Days e ținută în 2010.
Urmează versiuni noi până în zilele curente care aduc un flux constant de inovații. (cele mai mari 2.10 în 2013, 2.11 în 2014, 2.12 în 2016, 2.13 în 2019)
Motivul pentru decelerarea release-urilor în ultimii ani e faptul că creatorul Martin Oderski împreună cu stafful de la EPFL au început eforturile la Scala 3, cu numele de cod Dotty încă din 2013. După șapte ani de cercetare academică și muncă de implementare, Dotty devine Scala 3 și va fi lansat pană la finele acestui an în 2020.
Scala 3.0 este o rescriere bazată pe noi fundamente matematice (DOT calculus, SI calculus, lambda-circle) și vrea să distileze esența și misiunea Scala, eliminând concepte care nu s-au dovedit benefice în timp, reparând multe probleme existente în versiunea 2.0.
Rezultatul, un limbaj mai ușor pentru începători, mai estetic, natural și plăcut de folosit. Scala e un motor inovator pentru platforma JVM. Un vector de creștere atât pentru programatorii din acest ecosistem, cât și pentru limbajul Java în sine. Ca exemplu, "pattern matching"-ul o funcționalitate clasică din programarea funcțională și existentă în Scala din 2006 este programată pentru introducere în Java / JDK 15 prin JEP-375.
Poate e o subestimare să spun că această lansare e importantă pentru JVM. Aș putea aduce argumentul că e un moment important pentru domeniul programării în general, indiferent de platformă și ecosistem.
În continuare expunem câteva dintre noutăți.
val x = 10
def addTwo(i: Int) =
val x = 2
x + i
// addTwo(2) // error: expected a top level definition
val y = addTwo(2) // this is fine
Iată cât de elegant arată acum codul:
for x <- xs
y <- ys do
println(s"$x $y")
while x >= 0 do
x = f(x)
opt match
case Some(x) => f(x)
case None => default
try body
catch case ex => handler
class Dog(name: String):
def bark() = println("bark")
Spațiul semnificativ e o schimbare probabil controversată pentru programatorii care au trăit o lungă durată în ecosistemul Java, însă Oderski, creatorul limbajului, a testat acest stil (fără acolade) în ultima jumătate de an și spune că i-a adus o productivitate crescută.
@main def hello =
println("Hello world")
@main def hello(name: String) =
println(s"Hello $name")
Acestea sunt programe Scala complete. Nu e nevoie nici măcar de un import.
val t = ("a", "b", "c")
val i = 2
t(i)
val a, b, c = t
val xs: List[(Int, Int)]
xs.map {
(x, y) => x + y
}
// sau mai usor
xs.map(_ + _)
val dog = Dog("Max")
class Dog(name: String):
def this() = this("Astro")
Toate aceste elemente combinate fac din limbaj un competitor viabil în spațiul de scripting. Puterea compilatorului (2.0) în a elimina declarațiile ceremonioase de tipuri mi-a curbat demult entuziasmul de a folosi Python sau JS / TS în favoarea Scala. Însă aceste noi schimbări m-au convins complet că Scala 3.0 poate ocupa în întregime această funcție.
Scala 3 încearcă să restabilească balanța, introducând cuvântul cheie "export", care face posibilă selecția, copierea și redenumirea funcționalităților care se vor transmise:
class SmartPhone():
private val camera = new Camera()
private val phone = new Phone()
export camera.takePhoto
export phone.{makeCall => call}
(Acest lucru era posibil și anterior, însă cu mult mai multă ceremonie care implica definirea unei clase implicite.)
case class Circle(x: Double, y: Double,
radius: Double)
def (c: Circle) circumference: Double =
c.radius * math.Pi * 2
sau
extension (c: Circle):
def circumference: Double = c.radius * math.Pi * 2
Tipul se poate extinde cu mai multe metode odată, caz în care devine o extensie colectivă (collective extensions).
Inclusiv tipurile generice se pot extinde
(vom reveni asupra celorlalte elemente din acest exemplu la sfârșitul articolului)
def maximum[T](xs: List[T])(using Ord[T]) =
xs.reduce((x, y) => if (x < y) then y else x)
enum Color:
case Red, Green, Blue
enum Color(val rgb: Int):
case Red extends Color(0xFF0000)
case Green extends Color(0x00FF00)
case Blue extends Color(0x0000FF)
case Mix(mix: Int) extends Color(mix)
Așteptarea a meritat, depășind în ceea ce privește capabilitățile majoritatea altor limbaje.
Ele pot avea parametri de tip și metode, făcând astfel posibilă definirea ADT-urilor (tipuri de date abstracte) și a GADT-urilor (tipuri de date abstracte generalizate)
enum Option[+T]:
case Some(x: T)
case None
def isDefined: Boolean = this match
case None => false
case some => true
trait Animal(name: String):
def sound: String
def speak(): Unit = println(s"$name says $sound")
class Dog(name: String) extends Animal(name):
override def sound: String = "bark"
class Cat(name: String) extends Animal(name):
override def sound: String = "meow"
object MaxTheDog extends Dog("Max")
Două adăugări importante, cu o solidă fundamentare teoretică sunt:
trait Camera:
def takePhoto(): Photo
trait Phone:
def makeCall(): Unit
def useDevice(
smartphone: Camera & Phone
) =
smartphone.takePhoto()
smartphone.makeCall()
case class Human(name: String)
case class Bear(area: String)
case class Robot(id: Int)
def rideABicycle(
rider: Human | Bear | Robot
) = rider match
case Human(_) => println("meh")
case Bear(_) => println(":(")
case Robot(_) => println(":)")
(De luat în considerare: tip nu înseamnă doar trait (interfață) ci orice definiție de tip, inclusiv class, enum, tip abstract etc. )
Pentru cei care au rămas nelămuriți cu privire la faptul că A & B reprezintă o intersecție și nu o o uniune, precizăm că termenul se referă la valorile mulțimilor A și B din teoria seturilor. Sunt mai puține valori (instanțe) care satisfac constrângerile A & (și) B, de aici intersecția. Și mai multe valori (instanțe) care satisfac A | (sau) B.
Baza acestor tipuri este fundamentată în calculul DOT (dependent object type calculus), o inovație adusă în procesul de creare a Scala 3. Iată doar câteva proprietăți:
<: relație de subtip
=:= relație de egalitate de tip
Avantajul acestor tipuri e că putem să creăm asocieri ad-hoc, fără a fi necesar ca ele să facă parte dintr-o ierarhie sau definiție comună, încurajând buna modelare și abstractizare a programului, păstrând în același timp siguranța oferită de compilator.
Alte adiții noi, mai avansate în domeniul tipurilor sunt:
val s: "abc" = "abc"
s are tipul "abc", nu poate fi asignat la egal nimic altceva. Tipurile literale se pot avea doar pe ele înseși ca valori sau mai bine spus valorile sunt ele însele tipul.
Tipurile literale sunt mai utile împreună cu o construcție de uniune
def handleCommand(command: "help" | "info")
type ErrorOr[T] = T | "error"
object Context:
opaque type Name = String
val nameInside: Name = "Martin"
object Name:
def fromString(s: String) = Name(s)
import Context._
// val nameOutside: Name = "Martin"
// compile error
val nameOutside = Name.fromString("Martin") // OK
// val nameLength = nameOutside.length
// compile error
Înlocuiesc un feature anterior "clase de valori" (value classes) și satisfac aceeași nevoie de a defini un alias peste un tip existent (inclusiv primitiv) fără a plăti penalități de performanță survenite din (re)împachetarea lui (boxing si unboxing) .
Accesul la proprietățile tipului împachetat sunt ascunse (opace) în exteriorul contextului în care e definit tipul opac și sunt accesibile doar în contextul definiției sale.
type GenericMap = [V, K] =>> Map[K, V]
type StringKeyMap = [V] =>> Map[String, V]
val mm: GenericMap[Int, String] = Map[String, Int]()
val mi: StringKeyMap[Double] = Map[String, Double]()
Anterior nefiind posibila folosirea funcțiilor libere și pasarea tipului lor în program.
Ele fac posibilă definirea funcțiilor a căror tip de retur depinde de valoarea parametrilor funcției.
trait Entry:
type Key
type Value
val key: Key
val value: Value
def extractKey(e: Entry): e.Key = e.key
// dependent return type
val extractor: (e: Entry) => e.Key = extractKey
// dependent function type
Nici un limbaj comun (nici măcar unul avansat ca Haskell) nu are această funcționalitate, aducând Scala un pas spre limbaje de demonstrare automată ca și Agda sau Idris.
Una din cele mai puternice și benefice mecanisme regăsite în Scala 2, implicitele (implicits), a fost în totalitate refăcut. De multe ori abuzat de către experți, neînțeles de către începători a cauzat multă frustrare în folosirea Scala ca limbaj.
Ca să înțelegem noile schimbări, ar trebui expus puțin ce înseamnă o abstracție a contextului. O problemă des întâlnită în codul prost scris e accesarea contextului global, direct din funcție, sau în mod mai subtil, accesarea unor date care nu sunt declarate ca parametri la funcție. Aceasta face imprevizibil apelul unei funcții în timpul rulării, iar în timpul dezvoltării face neclară logica de business.
Programarea funcțională rezolvă această problemă prin mandatarea folosirii funcțiilor pure, care nu au voie să acceseze în afara listei de parametri formali din declarația funcției, forțându-ne, astfel, să transmitem contextul ca parametru la funcție.
Într-o înșiruire de apeluri devine puțin ridicol pasarea peste tot a acelorași parametri și a aceluiași context. Una din soluții ar fi omiterea contextului la momentul apelului de funcție, declarând doar că funcția are nevoie de un context, iar acest context / parametru să fie completat automat de către compilator.
Acest mecanism este întâlnit într-o formă sau alta în Haskell (implicit parameters), Rust (traits), Swift (protocol extensions). În C# și F# sunt doar la nivel de propunere.
În Scala 3.0 se poate folosi în următorul mod:
18: În definiția unei metode adăugăm o lista suplimentară de parametri prefixată de cuvântul cheie "using"
def max[T](x: T, y: T)(using ord: Ord[T]): T =
if ord.compare(x, y) < 0 then y else x
aici am definit necesitatea unei instanțe ord care poate compara elemente de tipul T.
Nefiind obligatoriu să dăm un nume, putem ruga compilatorul să ne dea o instanță de acest tip
def max[T](x: T, y: T)(using Ord[T]): T =
if summon[Ord[T]].compare(x, y) < 0 then y else x
este mai util când nu folosim instanța și o pasăm mai departe la rândul nostru, prin simpla existență.
def max[T](x: T, y: T)(using Ord[T]): T =
if summon[Ord[T]].compare(x, y) < 0 then y else x
trait Ord[T]:
def compare (x: T, y: T): Int
given Ord[Int]:
def compare (x: Int, y: Int) =
if (x < y) -1 else if (x > y) +1 else 0
Combinația using / givens este un mecanism de marcare special, pentru "invitarea" parametrilor în funcție.
E un mecanism simplu, dar esențial în implementarea unor tipare importante cum ar fi type classes, dependency injection și demonstrarea automată.
trait SemiGroup[T]:
def (x: T) combine (y: T): T
trait Monoid[T] extends SemiGroup[T]:
def unit: T
given Monoid[String]:
def (x: String) combine (y: String) = x.concat(y)
def unit = ""
def sum[T: Monoid](xs: List[T]): T =
xs.foldLeft(summon[Monoid[T]].unit)(_ combine _)
În acest exemplu nu este declarat explicit prin "using", ci declarat prin limita de context (context bound) "T: Monoid"
Nu avem spațiu pentru a detalia funcționalitățile noi de meta programare, însă merită menționat că ele au fost refăcute de la 0, sunt foarte puternice și vor contribui la dezvoltarea unui ecosistem de librării avansate.
Limbajul are suport în IDE-urile IntelliJ si Visual Code prin pluginul Metals.
Personal, cred că Scala, în ultima sa iterație 3.0, îndeplinește misiunea cu care a început acum 16 ani, adică să fie un limbaj unificator al paradigmelor, al nivelelor de experiență și al mărimilor de proiecte și, în același timp, să împingă (din nou) limitele.
V-aș recomanda să îl încercați, mai ales dacă ați simțit cândva ca programarea poate fi mai mult decât un job.