Futures Made Easy med Scala

Fremtiden er en abstraktion, der repræsenterer afslutningen af ​​en asynkron operation. I dag bruges det ofte på populære sprog fra Java til Dart. Men da moderne applikationer bliver mere komplekse, bliver det også vanskeligere at komponere dem. Scala bruger en funktionel tilgang, der gør det let at visualisere og konstruere fremtidig komposition.

Denne artikel har til formål at forklare det grundlæggende på en pragmatisk måde. Ingen jargon, ingen udenlandsk terminologi. Du behøver ikke engang at være Scala-programmør (endnu). Alt hvad du behøver at have er en vis forståelse af et par funktioner af højere orden: kort og foreach. Så lad os komme i gang.

I Scala kan en fremtid oprettes så simpelt som dette:

Future {"Hi"} 

Lad os nu køre det og lave en “Hej verden”.

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

Det er alt der er. Vi har lige kørt en fremtid ved hjælp af foreach, manipuleret resultatet lidt og udskrevet det til konsollen.

Men hvordan er det muligt? Så vi forbinder normalt foreach og kort med samlinger: vi pakker indholdet ud og fikser med det. Hvis du ser på det, ligner det konceptuelt en fremtid på den måde, vi ønsker at pakke output fra Future{}og manipulere det. For at få dette til at ske, skal fremtiden være afsluttet først og dermed "køre" den. Dette er ræsonnementet bag den funktionelle sammensætning af Scala Future.

I realistiske applikationer ønsker vi ikke kun at koordinere en, men flere futures på én gang. En særlig udfordring er, hvordan man arrangerer dem til at køre sekventielt eller samtidigt .

Sekventiel løb

Når flere futures starter efter hinanden som et stafetløb, kalder vi det sekventielt løb. En typisk løsning ville simpelthen være at placere en opgave i den tidligere opgaves tilbagekald, en teknik kendt som kædning. Konceptet er korrekt, men det ser ikke smukt ud.

I Scala kan vi bruge forforståelse til at hjælpe os med at abstrakte det. Lad os bare gå direkte til et eksempel for at se, hvordan det ser ud.

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 }

Den første ting at gøre er at importere ExecutionContext, hvis rolle er at styre trådpuljen. Uden den vil vores fremtid ikke løbe.

Dernæst definerer vi vores "store job", som simpelthen venter et sekund og returnerer dets input steget med et.

Så har vi vores forforståelsesblok. I denne struktur tildeler hver linje indeni et jobresultat til en værdi med &lt; - som derefter vil være tilgængelig for efterfølgende futures. Vi har arrangeret vores job, så bortset fra det første, hver tager output fra det forrige job.

Bemærk også, at resultatet af en forforståelse også er en fremtid med output bestemt af udbytte. Efter udførelsen vil resultatet være tilgængeligt indeni map. Til vores formål sætter vi simpelthen alle job outputs på en liste og tager dens størrelse.

Lad os køre det.

Vi kan se de fem futures fyret en efter en. Det er vigtigt at bemærke, at dette arrangement kun skal bruges, når fremtiden er afhængig af den tidligere fremtid.

Samtidig eller parallel kørsel

Hvis fremtiden er uafhængig af hinanden, skal de fyres samtidigt. Til dette formål skal vi bruge Future.sequence . Navnet er lidt forvirrende, men i princippet tager det simpelthen en liste over futures og omdanner det til en fremtid med listen. Evalueringen udføres dog asynkront.

Lad os skabe et eksempel på blandede sekventielle og parallelle futures.

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 tager en liste over futures, som vi ønsker at køre samtidigt. Så her har vi f2 og f4, der indeholder to parallelle job. Som argumentet fremført i Future.sequence er en liste, er resultatet også en liste. I en realistisk applikation kan resultaterne kombineres til yderligere beregning. Her tager vi det første element fra hver liste med .headderefter det til henholdsvis f3 og f5.

Lad os se det i aktion:

Vi kan se job i 2 og 4 fyret samtidigt, hvilket indikerer vellykket parallelisme. Det er værd at bemærke, at parallel udførelse ikke altid er garanteret, da det afhænger af tilgængelige tråde. Hvis der ikke er nok tråde, kører kun nogle af jobene parallelt. De andre vil dog vente, indtil nogle flere tråde frigøres.

Gendannelse efter fejl

Scala Future inkorporerer gendannelse, der fungerer som en back-up-fremtid, når der opstår en fejl . Dette gør det muligt for den fremtidige sammensætning at afslutte selv med fejl. For at illustrere, overvej denne kode:

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

Dette fungerer selvfølgelig ikke, da "abc" ikke er et int. Med Recover kan vi redde det ved at overføre en standardværdi. Lad os prøve at passere et nul:

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

Nu koden kører og producerer en som et resultat. I komposition kan vi finjustere hver fremtid som denne for at sikre, at processen ikke mislykkes.

Der er dog også tidspunkter, hvor vi udtrykkeligt vil afvise fejl. Til dette formål kan vi bruge Future.succesful og Future.failed til at signalere valideringsresultat. Og hvis vi ikke er interesserede i individuel fiasko, kan vi placere gendannelsen for at fange enhver fejl inde i kompositionen.

Lad os arbejde en anden bit kode ved hjælp af forforståelse, der kontrollerer, om input er en gyldig int og lavere end 100. Future.failed og Future.successful er begge futures, så vi behøver ikke at pakke den ind i en. Future.failed kræver især en kastbar, så vi opretter en brugerdefineret til input større end 100. Efter at have samlet det hele sammen, ville vi have som følger:

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

Bemærk placeringen af ​​inddrivelse. Med denne konfiguration opfanger den simpelthen enhver fejl, der opstår inde i blokken. Lad os teste det med flere forskellige indgange "5", "200" og "abc":

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

“5” nåede slutningen intet problem. "200" og "abc" ankom for at komme sig. Hvad nu, hvis vi vil håndtere hver fejl separat? Det er her mønstermatchning kommer i spil. Udvidelse af gendannelsesblokken kan vi have noget som dette:

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

You might probably have guessed it but an all-or-nothing scenario like this is commonly used in public APIs. Such service wouldn’t process invalid input but needs to return a message to inform the client what they did wrong. By separating exceptions, we can pass a custom message for each error. If you like to build such service (with a very fast web framework), head over to my Vert.x article.

The world outside Scala

We have talked a lot about how easy Scala Future is. But is it really? To answer it we need to look at how it’s done in other languages. Arguably the closest language to Scala is Java as both operate on JVM. Furthermore, Java 8 has introduced Concurrency API with CompletableFuture which is also able to chain futures. Let’s rework the first sequence example with it.

That’s sure a lot of stuff. And to code this I had to look up supplyAsync and thenApply among so many methods in the documentation. And even if I know all these methods, they can only be used within the context of the API.

On the other hand, Scala Future is not based on API or external libraries but a functional programming concept that is also used in other aspects of Scala. So with an initial investment in covering the fundamentals, you can reap the reward of less overhead and higher flexibility.

Wrapping up

Det er alt for det grundlæggende. Der er mere ved Scala Future, men hvad vi har her, har dækket nok grund til at opbygge virkelige applikationer. Hvis du gerne vil læse mere om Future eller Scala, vil jeg generelt anbefale Alvin Alexander-tutorials, AllAboutScala og Sujit Kamthe's artikel, der giver let at forstå forklaringer.