Futuurid on Scalaga lihtsaks tehtud

Tulevik on abstraktsioon, mis tähistab asünkroonse toimingu lõpuleviimist. Tänapäeval kasutatakse seda populaarsetes keeltes alates Java-st kuni Dartini. Kuna aga kaasaegsed rakendused muutuvad keerukamaks, on ka nende koostamine keerulisem. Scala kasutab funktsionaalset lähenemist, mis muudab tuleviku kompositsiooni visualiseerimise ja konstrueerimise lihtsaks.

Selle artikli eesmärk on selgitada põhitõdesid pragmaatiliselt. Ei kõnepruuki, ei võõrast terminoloogiat. Sa ei pea olema isegi Scala programmeerija (veel). Vaja on vaid mõningaid arusaamu paarist kõrgema järgu funktsioonist: kaart ja foreach. Nii et alustame.

Scalas saab tuleviku luua nii lihtsana kui see:

Future {"Hi"} 

Nüüd käivitame selle ja teeme "Tere maailm".

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

See on kõik, mis seal on. Me lihtsalt jooksime tulevikku kasutades foreach, manipuleerisime tulemusega natuke ja printisime selle konsooli.

Aga kuidas see on võimalik? Seega seostame foreachi ja kaardi tavaliselt kollektsioonidega: pakime sisu lahti ja nokitseme sellega. Kui seda vaadata, on see kontseptuaalselt sarnane tulevikuga viisil, mille abil soovime väljundi lahti pakkida Future{}ja sellega manipuleerida. Selle saavutamiseks tuleb tulevik kõigepealt lõpule viia, seega seda "juhtida". See on Scala Future funktsionaalse koostise põhjus.

Realistlikes rakendustes tahame kooskõlastada mitte ainult ühte, vaid mitut tulevikku korraga. Konkreetne väljakutse on see, kuidas korraldada nende järjestikune või samaaegne käitamine .

Järjestikune jooks

Kui mitmed tulevikud alustavad üksteise järel nagu teatevõistlus, nimetame seda järjestikuseks jooksuks. Tüüpiline lahendus oleks lihtsalt ülesande lisamine eelmise ülesande tagasihelistamisse, see on tehnika, mida nimetatakse aheldamiseks. Kontseptsioon on õige, kuid see ei tundu ilus.

Scalas saame selle mõistmiseks abiks olla mõistmiseks. Et näha, kuidas see välja näeb, läheme lihtsalt näite juurde.

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  println(s"Done. ${z.size} jobs run")) Thread.sleep(6000) // needed to prevent main thread from quitting // too early }

Esimene asi, mida teha, on importida ExecutionContext, mille roll on hallata lõimupooli. Ilma selleta ei jookse meie tulevik.

Järgmisena määratleme oma "suure töö", mis lihtsalt ootab sekundi ja tagastab sisendi ühe võrra suurendatuna.

Siis on meil arusaamise blokeering. Selles struktuuris määrab iga sees olev rida töö tulemuse väärtusega &lt; -, mis on seejärel saadaval kõigi järgnevate futuuride jaoks. Oleme oma töökohad korraldanud nii, et välja arvatud esimene, võtab igaüks endise töö väljundi.

Pange tähele, et mõistmise tulemus on ka tulevik, mille väljund määratakse saagikuse järgi. Pärast hukkamist on tulemus saadaval sees map. Sel eesmärgil paneme lihtsalt kõigi tööde väljundid loendisse ja võtame selle suuruse.

Käivitame selle.

Näeme, kuidas viis tulevikku tulistati ükshaaval. Oluline on märkida, et seda korda tuleks kasutada ainult siis, kui tulevik sõltub eelmisest tulevikust.

Samaaegne või paralleeljooks

Kui futuurid on üksteisest sõltumatud, tuleks neid vallandada üheaegselt. Sel eesmärgil kasutame Future.sequence . Nimi on natuke segane, kuid põhimõtteliselt võtab see lihtsalt futuuride nimekirja ja muudab selle loendi tulevikuks. Hindamine toimub aga asünkroonselt.

Loome näite segast järjestikust ja paralleelset tulevikku.

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  println(s"Done. $z jobs run in parallel"))

Future.sequence võtab nimekirja tulevikest, mida soovime käivitada üheaegselt. Nii et siin on meil f2 ja f4, mis sisaldavad kahte paralleelset tööd. Kuna Future.sequence'i sisestatud argument on loend, on tulemus ka loend. Realistlikus rakenduses võib tulemusi edaspidiseks arvutamiseks kombineerida. Siin võtame iga loendi esimese elemendi .headja edastame selle vastavalt f3 ja f5.

Vaatame seda toimimas:

Näeme, et töökohad kahes ja neljas vallandati üheaegselt, mis näitab edukat paralleelsust. Väärib märkimist, et paralleelne täitmine pole alati tagatud, kuna see sõltub saadaolevatest lõimedest. Kui lõime ei ole piisavalt, töötavad paralleelselt ainult mõned tööd. Teised aga ootavad, kuni veel mõned lõimed vabanevad.

Vigadest taastumine

Scala Future sisaldab taastamist, mis toimib vea ilmnemisel varutulevikuna . See võimaldab tulevasel koosseisul lõpetada ka ebaõnnestumistega. Selle illustreerimiseks kaaluge seda koodi:

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

Loomulikult see ei toimi, kuna “abc” pole int. Mis taastuda, saame päästa ta sooritades vaikeväärtus. Proovime nulli ületada:

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

Nüüd töötab kood ja selle tulemusena üks. Kompositsioonis saame iga tuleviku niimoodi peenhäälestada, et protsess ei nurjuks.

Siiski on ka olukordi, kus tahame vead sõnaselgelt tagasi lükata. Sel eesmärgil saame valideerimise tulemuse signaalimiseks kasutada Future.succesful ja Future.failed . Ja kui me ei hooli üksikute jätmise saame ametikohale tagasi püüda mis tahes viga sees kompositsiooni.

Töötame veel ühe koodibitiga, kasutades mõistmist, mis kontrollib, kas sisend on kehtiv int ja väiksem kui 100. Future.failed ja Future.successful on mõlemad futuurid, nii et me ei pea seda ühte pakkima. Future.failed eelkõige nõuab Throwable nii me läheme luua kohandatud üks sisend suurem kui 100. Pärast pannes kõik üles koos meil oleks järgmine:

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

Pange tähele taastamise positsioneerimist. Selle konfiguratsiooni korral saab see lihtsalt blokeerimisega seotud vead kinni. Testime seda mitme erineva sisendiga „5”, „200” ja „abc”:

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

"5" jõudis lõpuni pole probleemi. "200" ja "abc" saabusid taastuma. Mis siis saab, kui tahame iga viga eraldi käsitleda? Siin tuleb mängu mustrite sobitamine. Taasteploki laiendamisel võib meil olla midagi sellist:

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 } }

Võib-olla olete seda arvanud, kuid avalikes API-des kasutatakse tavaliselt sellist kõike või mitte midagi. Selline teenus ei töötle kehtetut sisendit, kuid peab tagastama sõnumi, et klienti teavitada valesti. Erandite eraldamisega võime iga vea kohta edastada kohandatud teate. Kui teile meeldib sellist teenust (väga kiire veebiraamistikuga) ehitada, minge minu artiklisse Vert.x.

Maailm väljaspool Scalat

Oleme palju rääkinud sellest, kui lihtne on Scala tulevik. Aga kas tõesti? Sellele vastamiseks peame uurima, kuidas seda teistes keeltes tehakse. Väidetavalt on Scalale kõige lähem keel Java, kuna mõlemad töötavad JVM-is. Lisaks on Java 8 kasutusele võtnud Concurrency API koos CompletableFuture'iga, mis on samuti võimeline aheldama futuure. Töötame sellega ümber esimese järjestusenäite.

See on kindel, et palju asju. Ja selle kodeerimiseks pidin dokumentatsioonis leidma nii paljude meetodite hulgast supplyAsync ja seejärel rakendama. Ja isegi kui ma tean kõiki neid meetodeid, saab neid kasutada ainult API kontekstis.

Teiselt poolt ei põhine Scala Future API-l ega välistel teekidel, vaid funktsionaalsel programmeerimiskontseptsioonil, mida kasutatakse ka Scala muudes aspektides. Nii et kui teha alginvesteering põhialuste katmiseks, saate kasu vähem üldkuludest ja suuremast paindlikkusest.

Pakkimine

See on kõik põhitõdesid. Scala Future'is on veel midagi, kuid see, mis meil siin on, on tegeliku elu rakenduste loomiseks piisavalt palju katnud. Kui soovite rohkem lugeda Future'i või Scala kohta, siis soovitaksin üldiselt Alvin Alexanderi õpetusi, AllAboutScala ja Sujit Kamthe artiklit, mis pakub hõlpsasti mõistetavaid selgitusi.