A futurák megkönnyítik a Scalát

A jövő egy absztrakció, amely egy aszinkron művelet befejezését jelenti. Ma általában a népszerű nyelveken használják, a Java-tól a Dartig. Mivel azonban a modern alkalmazások egyre bonyolultabbak, az összeállításuk is nehezebbé válik. A Scala funkcionális megközelítést alkalmaz, amely megkönnyíti a jövő kompozícióinak vizualizálását és elkészítését.

Ez a cikk pragmatikus módon kívánja elmagyarázni az alapokat. Nincs szakzsargon, nincs idegen terminológia. Nem is kell Scala programozónak lenni (még). Mindössze néhány magasabb rendű funkció megértése szükséges: térkép és foreach. Tehát kezdjük.

A Scalában egy ilyen egyszerű jövő hozható létre:

Future {"Hi"} 

Most futtassuk és készítsünk egy „Szia Világot”.

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

Ennyi van. Csak futottunk egy jövőt felhasználva foreach, kicsit manipuláltuk az eredményt, és kinyomtattuk a konzolra.

De hogyan lehetséges? Tehát általában a foreach és a térkép a gyűjteményekhez társul: kibontjuk a tartalmat és bütykölünk vele. Ha ránézünk, fogalmilag hasonló egy jövőhöz, ahogyan a kimenetet kibontani Future{}és manipulálni szeretnénk . Ahhoz, hogy ez megtörténjen, a jövőt előbb be kell fejezni, tehát „futtatni” kell. Ez az oka a Scala Future funkcionális összetételének.

A reális alkalmazásokban nem csak egy, hanem több jövőt is egyszerre akarunk összehangolni. Különleges kihívás az, hogyan lehet őket egymás után vagy egyszerre futtatni .

Szekvenciális futás

Amikor több jövő egymás után indul, mint egy váltóverseny, akkor ezt szekvenciális futásnak nevezzük. Tipikus megoldás egyszerűen egy feladat elhelyezése az előző feladat visszahívásában, a láncolásnak nevezett technikában. A koncepció helyes, de nem tűnik szépnek.

A Scalában a megértéshez használhatjuk az elvonatkoztatást. Hogy lássuk, hogyan néz ki, menjünk egyenesen egy példához.

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 }

Az első dolog az ExecutionContext importálása, amelynek feladata a szálkészlet kezelése. Enélkül a jövőnk nem fog futni.

Ezután meghatározzuk a „nagy munkánkat”, amely egyszerűen vár egy másodpercre, és eggyel növeli a bemenetét.

Aztán megvan a megértés blokk. Ebben a struktúrában a belső sorok a munka eredményét hozzárendelik egy &lt; - értékhez, amely elérhető lesz minden későbbi jövőre vonatkozóan. A munkánkat úgy rendeztük el, hogy az első kivételével mindegyik felvegye az előző munka kimenetét.

Vegye figyelembe azt is, hogy a megértés eredménye egy olyan jövő is, amelynek kimenetét a hozam határozza meg . A végrehajtás után az eredmény belül lesz elérhető map. Célunk érdekében egyszerűen felsoroljuk a munkák összes kimenetét, és felvesszük a méretét.

Futtassuk.

Láthatjuk, hogy az öt határidőt egyesével kilőtték. Fontos megjegyezni, hogy ezt a megállapodást csak akkor szabad alkalmazni, ha a jövő az előző jövőtől függ.

Egyidejű vagy párhuzamos futás

Ha a jövők függetlenek egymástól, akkor egyszerre kell őket lőni. Erre a célra a Future.sequence-t fogjuk használni . A név kissé zavaró, de elvileg egyszerűen felvesz egy jövőbeli listát, és átalakítja a lista jövőjévé. Az értékelést azonban aszinkron módon végzik.

Készítsünk egy példát vegyes szekvenciális és párhuzamos jövőre.

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

A Future.sequence felsorolja azokat a jövõket, amelyeket egyszerre szeretnénk futtatni. Tehát itt van f2 és f4, amelyek két párhuzamos munkát tartalmaznak. Mivel a Future.sequence argumentum egy lista, az eredmény egyben lista is. Reális alkalmazásban az eredmények kombinálhatók a további számításokhoz. Itt minden lista első elemét felvesszük, .headmajd továbbítjuk az f3-nak és az f5-nek.

Lássuk működés közben:

Egyszerre láthatjuk, hogy a 2. és a 4. csoport elbocsátott munkahelyek sikeres párhuzamosságra utalnak. Érdemes megjegyezni, hogy a párhuzamos végrehajtás nem mindig garantált, mivel ez a rendelkezésre álló szálaktól függ. Ha nincs elég szál, akkor csak a feladatok egy része fut párhuzamosan. A többiek azonban megvárják, amíg még néhány szál felszabadul.

Helyreállítás a hibákból

A Scala Future olyan helyreállítást tartalmaz , amely tartalék jövőként működik, ha hiba lép fel . Ez lehetővé teszi, hogy a jövőbeli kompozíció még kudarcokkal is befejeződjön. Illusztrációként vegye figyelembe ezt a kódot:

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

Természetesen ez nem fog működni, mivel az „abc” nem int. A helyreállítással megmenthetünk egy alapértelmezett érték átadásával. Próbáljunk meg nullát adni:

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

Most a kód futni fog, és ennek eredményeként előállít egyet. Összetételünkben minden jövőt így finomhangolhatunk, hogy megbizonyosodhassunk arról, hogy a folyamat nem fog kudarcot vallani.

Vannak azonban olyan esetek is, amikor kifejezetten el akarjuk utasítani a hibákat. Erre a célra használhatjuk a Future.succesful és a Future.failed elemeket az érvényesítési eredmény jelzésére. És ha nem törődnek az egyéni hiba tudjuk helyzetvisszanyerő fogni bármilyen hibát belül a készítmény.

Dolgozzunk egy másik bit kóddal a megértés használatával, amely ellenőrzi, hogy a bemenet érvényes int és alacsonyabb-e, mint 100. A Future.failed és a Future.successful egyaránt futures, így nem kell egybe csomagolnunk. Különösen a Future.failed esetében van szükség egy dobásra, így létrehozunk egy egyedi 100-nál nagyobb bemenetet. Miután az összeset összeállítottuk, a következők lennének:

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

Figyelje meg a helyreállítás helyzetét. Ezzel a konfigurációval egyszerűen elfogja a blokkon belül fellépő hibákat. Teszteljük több különböző bemenettel: „5”, „200” és „abc”:

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

Az „5” a végére ért, semmi gond. A „200” és az „abc” helyreállva érkezett. És mi van, ha minden hibát külön akarunk kezelni? Itt jön létre a mintaillesztés. A helyreállítási blokk kibővítésével valami ilyesmi lehet:

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

Valószínűleg kitalálta, de a nyilvános API-kban általában egy ilyen mindent vagy semmit tartalmazó forgatókönyvet használnak. Az ilyen szolgáltatás nem dolgozna fel érvénytelen bevitelt, de vissza kell küldenie egy üzenetet, hogy tájékoztassa az ügyfelet arról, hogy mit tett rosszul. A kivételek szétválasztásával minden hibához egyedi üzenetet adhatunk át. Ha szeretnél ilyen szolgáltatást felépíteni (nagyon gyors webes keretrendszerrel), akkor térj át a Vert.x cikkemre.

A Scala-n kívüli világ

Sokat beszéltünk arról, milyen könnyű a Scala Future. De vajon tényleg? Ahhoz, hogy megválaszolhassuk, meg kell vizsgálnunk, hogyan történik ez más nyelveken. A Scalához állíthatóan a legközelebbi nyelv a Java, mivel mindkettő JVM-en működik. Ezenkívül a Java 8 bevezette a Concurrency API-t a CompletableFuture- val, amely képes a jövők láncolására is. Dolgozzuk át vele az első szekvencia példát.

Ez biztos sok minden. Ennek kódolásához meg kellett keresnem a supplyAsync-et, majd az alkalmazást a dokumentációban szereplő sok módszer között. És még akkor is, ha ismerem ezeket a módszereket, csak az API kontextusában használhatók.

Másrészt a Scala Future nem API-n vagy külső könyvtárakon alapul, hanem egy funkcionális programozási koncepció, amelyet a Scala egyéb aspektusaiban is használnak. Tehát a fundamentumok fedezésére irányuló kezdeti befektetéssel kevesebb rezsivel és nagyobb rugalmassággal járhat.

Csomagolás

That’s all for the basics. There’s more to Scala Future but what we have here has covered enough ground to build real-life applications. If you like to read more about Future or Scala, in general, I’d recommend Alvin Alexander tutorials, AllAboutScala, and Sujit Kamthe’s article that offers easy to grasp explanations.