de Martin Budi

Futures ușor cu Scala

Futures usor cu Scala

Viitorul este o abstractizare care reprezintă finalizarea unei operații asincrone. Astăzi este utilizat în mod obișnuit în limbile populare de la Java la Dart. Cu toate acestea, pe măsură ce aplicațiile moderne devin mai complexe, compunerea lor devine tot mai dificilă. Scala utilizează o abordare funcțională care face mai ușoară vizualizarea și construirea compoziției viitoare.

Acest articol își propune să explice elementele de bază într-un mod pragmatic. Fără jargon, fără terminologie străină. Nici măcar nu trebuie să fii programator Scala (încă). Tot ce trebuie să aveți este o oarecare înțelegere a câteva funcții de ordin superior: hartă și foreach. Asadar, haideti sa începem.

În Scala, un viitor poate fi creat la fel de simplu ca acesta:

Future {"Hi"} 

Acum, să-l rulăm și să facem o „Bună lume”.

Future {"Hi"} .foreach (z => println(z + " World"))

Asta e tot ce există. Tocmai am condus un viitor folosind foreach, a manipulat puțin rezultatul și l-a imprimat pe consolă.

Dar cum este posibil? Așadar, în mod normal, asociem fiecare colecție și harta cu colecțiile: desfacem conținutul și jucăm cu el. Dacă te uiți la el, este similar din punct de vedere conceptual cu un viitor în felul în care dorim să dezvăluim rezultatul Future{}și manipulați-l. Pentru ca acest lucru să se întâmple, viitorul trebuie să fie finalizat mai întâi, deci „să îl rulăm”. Acesta este raționamentul din spatele compoziției funcționale a Scala Future.

În aplicații realiste, vrem să coordonăm nu doar unul, ci mai multe contracte futures simultan. O provocare specială este cum să le aranjați să ruleze secvențial sau simultan.

Executare secvențială

Când mai multe futures încep unul după altul, ca o cursă de ștafetă, o numim cursă secvențială. O soluție tipică ar fi pur și simplu plasarea unei sarcini în callback-ul sarcinii anterioare, o tehnică cunoscută sub numele de înlănțuire. Conceptul este corect, dar nu arată frumos.

În Scala, putem folosi for-comprehension pentru a ne ajuta să o abstractizăm. Pentru a vedea cum arată, să mergem direct la un exemplu.

import scala.concurrent.ExecutionContext.Implicits.global

object Main extends App {

  def job(n: Int) = Future {
    Thread.sleep(1000)
    println(n) // for demo only as this is side-effecting 
    n + 1
  }

  val f = for {
    f1 <- job(1)
    f2 <- job(f1)
    f3 <- job(f2)
    f4 <- job(f3)
    f5 <- job(f4)
  } yield List(f1, f2, f3, f4, f5)
  f.map(z => println(s"Done. ${z.size} jobs run"))
  Thread.sleep(6000) // needed to prevent main thread from quitting 
                     // too early 
}

Primul lucru de făcut este să importați ExecutionContext al cărui rol este de a gestiona grupul de fire. Fără el, viitorul nostru nu va funcționa.

Apoi, ne definim „marea treabă” care pur și simplu așteaptă o secundă și returnează intrarea sa incrementată cu una.

Apoi avem blocul nostru de înțelegere. În această structură, fiecare linie din interior atribuie rezultatul unui job unei valori cu &lt; – care va fi apoi disponibil pentru orice viitor ulterior. Ne-am aranjat joburile astfel încât, cu excepția primei, fiecare să preia rezultatul jobului anterior.

De asemenea, rețineți că rezultatul unei înțelegeri este, de asemenea, un viitor cu rezultatul determinat de Randament. După executare, rezultatul va fi disponibil în interior map. În scopul nostru, pur și simplu punem toate rezultatele joburilor într-o listă și luăm dimensiunea acesteia.

Să-l rulăm.

Futures usor cu Scala
Executare secvențială

Putem vedea cele cinci contracte futures lansate unul câte unul. Este important să rețineți că acest aranjament trebuie utilizat numai atunci când viitorul depinde de viitorul anterior.

Alergare simultană sau paralelă

Dacă viitorul este independent unul de celălalt, atunci ar trebui concediat simultan. În acest scop, vom folosi Viitor.consecință. Numele este un pic confuz, dar, în principiu, pur și simplu ia o listă de contracte futures și o transformă într-un viitor de listă. Cu toate acestea, evaluarea se face asincron.

Să creăm un exemplu de futures mixte secvențiale și paralele.

val f = for {
  f1 <- job(1)
  f2 <- Future.sequence(List(job(f1), job(f1)))
  f3 <- job(f2.head)
  f4 <- Future.sequence(List(job(f3), job(f3)))
  f5 <- job(f4.head)
} yield f2.size + f4.size
f.foreach(z => println(s"Done. $z jobs run in parallel"))

Future.sequence ia o listă de contracte futures pe care dorim să le rulăm simultan. Deci aici avem f2 și f4 conținând două joburi paralele. Deoarece argumentul introdus în Future.sequence este o listă, rezultatul este și o listă. Într-o aplicație realistă, rezultatele pot fi combinate pentru calcul ulterior. Aici vom lua primul element din fiecare listă cu .head apoi treceți-o la f3 și respectiv la f5.

Să o vedem în acțiune:

1611994687 19 Futures usor cu Scala
Alergare paralelă

Putem vedea joburile din 2 și 4 concediate simultan, indicând un paralelism reușit. Este demn de remarcat faptul că execuția paralelă nu este întotdeauna garantată, deoarece depinde de firele disponibile. Dacă nu există suficiente fire, atunci doar unele dintre lucrări vor rula în paralel. Ceilalți, cu toate acestea, vor aștepta până când vor mai fi eliberate câteva fire.

Recuperarea după erori

Scala Future încorporează recupera care acționează ca un viitor de rezervă atunci când apare o eroare. Acest lucru permite viitoarei compoziții să se termine chiar și cu eșecuri. Pentru a ilustra, luați în considerare acest cod:

Future {"abc".toInt}
.map(z => z + 1)

Desigur, acest lucru nu va funcționa, deoarece „abc” nu este un int. Cu recupera, îl putem salva trecând o valoare implicită. Să încercăm să trecem un zero:

Future {"abc".toInt}
.recover {case e => 0}
.map(z => z + 1)

Acum, codul va rula și va produce unul ca urmare. În compoziție, putem regla fiecare viitor astfel, pentru a ne asigura că procesul nu va eșua.

Cu toate acestea, există și momente în care vrem să respingem în mod explicit erorile. În acest scop, putem folosi Viitorul.uccesiv și Viitorul eșuat pentru a semnaliza rezultatul validării. Și dacă nu ne pasă de eșecul individual, putem poziționa recuperarea pentru a prinde orice eroare în interiorul compoziției.

Să lucrăm încă un pic de cod folosind for-comprehension, care verifică dacă intrarea este un int valid și mai mic de 100. Future.failed și Future.successful sunt ambele futures, deci nu este nevoie să-l înfășurăm într-unul. Viitorul eșuat necesită în special un Aruncabil așa că vom crea unul personalizat pentru intrări mai mari de 100. După ce am pus totul împreună vom avea următoarele:

val input = "5" // let's try "5", "200", and "abc"
case class NumberTooLarge() extends Throwable()
val f = for {
   f1 <- Future{ input.toInt }
   f2 <- if (f1 > 100) {
            Future.failed(NumberTooLarge())
          } else {
            Future.successful(f1)
          }
} yield f2
f map(println) recover {case e => e.printStackTrace()}

Observați poziționarea recuperării. Cu această configurație, va intercepta pur și simplu orice eroare care apare în interiorul blocului. Să-l testăm cu mai multe intrări diferite „5”, „200” și „abc”:

"5"   -> 5
"200" -> NumberTooLarge stacktrace
"abc" -> NumberFormatException stacktrace 

„5” a ajuns la final nici o problemă. „200” și „abc” au ajuns în recuperare. Acum, dacă vrem să gestionăm fiecare eroare separat? Aici intră în joc potrivirea modelelor. Extindând blocul de recuperare, putem avea așa ceva:

case e => 
  e match {
    case t: NumberTooLarge => // deal with number > 100
    case t: NumberFormatException => // deal with not a number
    case _ => // deal with any other errors
  }
}

S-ar putea să fi ghicit probabil, dar un scenariu totul sau nimic este folosit în mod obișnuit în API-urile publice. Un astfel de serviciu nu va procesa date nevalide, dar trebuie să returneze un mesaj pentru a informa clientul despre ce a greșit. Prin separarea excepțiilor, putem transmite un mesaj personalizat pentru fiecare eroare. Dacă vă place să creați un astfel de serviciu (cu un cadru web foarte rapid), mergeți la my Articol Vert.x.

Lumea din afara Scalei

Am vorbit mult despre cât de ușor este Scala Future. Dar chiar este? Pentru a răspunde, trebuie să ne uităm la modul în care se face în alte limbi. Probabil că limbajul cel mai apropiat de Scala este Java, deoarece ambele funcționează pe JVM. Mai mult, Java 8 a introdus API-ul concurenței cu CompletableFuture care este, de asemenea, capabil să lanseze viitorul. Să refacem primul exemplu de secvență cu el.

1611994688 905 Futures usor cu Scala
Executare secvențială în Java

E sigur că sunt multe lucruri. Și pentru a codifica acest lucru, a trebuit să mă uit în sus supplyAsync și apoi Aplicați printre atâtea metode din documentație. Și chiar dacă știu toate aceste metode, ele pot fi utilizate numai în contextul API-ului.

Pe de altă parte, Scala Future nu se bazează pe API sau biblioteci externe, ci pe un concept de programare funcțional care este utilizat și în alte aspecte ale Scala. Așadar, cu o investiție inițială în acoperirea fundamentelor, puteți obține recompensa unei flexibilități mai reduse și a unei flexibilități mai mari.

Înfășurându-se

Asta este doar pentru elementele de bază. Scala Future are mai multe, dar ceea ce avem aici a acoperit suficient teren pentru a construi aplicații din viața reală. Dacă doriți să citiți mai multe despre Future sau Scala, în general, vă recomand Tutoriale Alvin Alexander, AllAboutScala, și Articolul lui Sujit Kamthe care oferă explicații ușor de înțeles.