ABONAMENTE VIDEO REDACȚIA
RO
EN
×
▼ LISTĂ EDIȚII ▼
Numărul 100
Abonament PDF

Scala 3: cel mai important eveniment JVM din 2020

Florin Bunău
Senior Software Engineer @ ComplyAdvantage
PROGRAMARE


Ț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 !

Ce face Scala diferit?

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.)

Mic istoric Scala. De ce lansarea Scala 3 e cel mai mare eveniment pe JVM din 2020?

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.

Simplificări

  1. Acum putem defini fără clase, orice, direct în fișier (toplevel definitions), așa cum am fost obișnuiți de limbaje mai prietenoase de scripting ca Javascript sau Python
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
  1. Încă o schimbare pe care poate ați observat-o. Parantezele pentru blocuri nu mai sunt necesare, sunt complet opționale (pentru fiecare bloc), blocurile putând fi definite prin indentare.

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ă.

  1. A fost simplificat și modul de a crea un punct de intrare în program prin adăugarea unei adnotări.
@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.

  1. Elementele tuplelor pot fi accesate după index, și despachetate mai ușor:
val t = ("a", "b", "c")
val i = 2
t(i)

val a, b, c = t
  1. Lucrul cu tuple a fost făcut și mai ușor de îmbunătățit la destructurarea lor
val xs: List[(Int, Int)]
xs.map {
  (x, y) => x + y
}

// sau mai usor
xs.map(_ + _)
  1. Clasele pot fi instanțiate fără cuvântul cheie "new". chiar dacă nu sunt "case class"
val dog = Dog("Max")
  1. Adăugarea constructorilor adiționali a fost simplificată, realizându-se prin cuvântul special this
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.

Expresivitate în definirea tipurilor de date

  1. Probabil una din adițiile mele favorite este introducerea unui mecanism de a exprima agregarea / compoziția de tipuri. Nu am văzut până acum vreun limbaj să susțină adagiul: "Preferă compoziție față de moștenire" prin vreo facilitate. Ar putea fi argumentat că reputația proastă pe care o are programarea orientată obiect survine din folosirea defectuoasă a moștenirii, iar limbajele sunt de vină pentru înlesnirea acestui tipar prin punerea la dispoziție a cuvintelor cheie extends, implements etc.

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}
  1. O-ul din SOLID, "Închis pentru modificare, dar deschis pentru extensie", primește puțin ajutor prin adăugarea "extensiilor de metode" (extension methods) declarate prin cuvântul cheie "extension". Unui tip de date i se pot adăuga metode, după definirea sa, apelarea lor realizându-se în mod normal prin "." (infix)

(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)
  1. Ceva foarte comun în multe limbaje, însă lipsit până acum din Scala sunt enumerările.
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
  1. Traiturile (~ similare cu interfețele), acum, suportă parametri, ușurând implementarea lor:
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:

  1. Intersecția de tipuri (intersection types)
trait Camera:
  def takePhoto(): Photo

trait Phone:
  def makeCall(): Unit

def useDevice(
  smartphone: Camera & Phone
) =
  smartphone.takePhoto()
  smartphone.makeCall()
  1. Uniunea de tipuri (union types)
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:

  1. Tipurile literale (literal types)
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"
  1. Tipurile opace (opaque types) - marcate prin cuvântul cheie opaque
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.

Tipuri funcție avansate

  1. Programarea la nivel de tipuri (typelevel programming) a fost până acum greoaie din cauza lipsei funcțiilor de tip anonime (type lambdas). Existența lor face acum posibilă manipularea abstractă a tipurilor pentru a forma noi tipuri.
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]()
  1. "Metodele dependente de tip" (dependent methods) existente deja în Scala, au fost generalizate în forma de "funcții dependente de tip" (dependent function types) .

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.

Abstracții ale contextului

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
  1. Declarăm o implementare a tipului și o marcăm cu cuvântul cheie "givens"
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"

Metaprogramare

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.

Suport IDE

Limbajul are suport în IDE-urile IntelliJ si Visual Code prin pluginul Metals.

Concluzie

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.

Referințe:

  1. Dotty / Scala 3 documentation
  2. Rock the JVM
  3. Countdown to Scala 3 by Martin Odersky
  4. What's coming in Scala 3 by Josh Suereth & James Ward
  5. GOTO 2020 Kotlin 4 vs. Scala 3 by Garth Gilmour & Eamonn Boyle

LANSAREA NUMĂRULUI 101

Prezentări articole și Panel:
Democratizarea machine learning în Cloud

Vineri, 27 Noiembrie, ora 18:00

Înregistrează-te

Facebook Meetup Live on StreamEvent.ro

VIDEO: NUMĂRULUI 100

Sponsori

  • comply advantage
  • ntt data
  • Betfair
  • Accenture
  • Siemens
  • Bosch
  • FlowTraders
  • MHP
  • Connatix
  • Cognizant Softvision
  • BoatyardX
  • Colors in projects