עתידיים קלים עם סקאלה

העתיד הוא הפשטה המייצגת השלמה של פעולה א-סינכרונית. כיום משתמשים בו בדרך כלל בשפות פופולריות מג'אווה ועד חץ. עם זאת, מכיוון שיישומים מודרניים הופכים מורכבים יותר, גם הרכבתם הופכת לקשה יותר. Scala משתמש בגישה פונקציונלית שמאפשרת לדמיין ולבנות קומפוזיציה עתידית.

מאמר זה נועד להסביר את היסודות בצורה פרגמטית. אין ז'רגון, אין טרמינולוגיה זרה. אתה אפילו לא צריך להיות מתכנת Scala (עדיין). כל מה שאתה צריך זה קצת הבנה של כמה פונקציות מסדר גבוה יותר: מפה ו- foreach. אז בואו נתחיל.

בסקאלה ניתן ליצור עתיד פשוט כזה:

Future {"Hi"} 

עכשיו בואו ננהל אותו ונעשה "היי עולם".

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

זה כל מה שיש. פשוט רצנו עתיד באמצעות foreach, טיפלנו קצת בתוצאה והדפסנו אותה לקונסולה.

אבל איך זה אפשרי? אז בדרך כלל אנו מקשרים בין foreach ומפה לאוספים: אנו פורשים את התוכן ומתעסקים בו. אם אתה מסתכל על זה, זה דומה מבחינה רעיונית לעתיד באופן שאנחנו רוצים לפרוש את הפלט ממנו Future{}ולתפעל אותו. כדי שזה יקרה, העתיד צריך להסתיים תחילה, ומכאן "להריץ" אותו. זה הנימוק מאחורי ההרכב הפונקציונלי של Scala Future.

ביישומים מציאותיים, אנו רוצים לתאם לא רק אחד אלא כמה עתיד בבת אחת. אתגר מסוים הוא כיצד לארגן אותם לרוץ ברצף או בו זמנית .

ריצה עוקבת

כאשר כמה עתידיים מתחילים בזה אחר זה כמו מירוץ ממסר אנו קוראים לו ריצה עוקבת. פיתרון אופייני פשוט הוא הצבת משימה בשיחת החזרה של המשימה הקודמת, טכניקה המכונה שרשור. הרעיון נכון אך הוא לא נראה יפה.

בסקאלה נוכל להשתמש בהבנת מידע כדי לעזור לנו להפשט אותה. כדי לראות איך זה נראה, בואו נעבור ישר לדוגמא.

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 }

הדבר הראשון שיש לעשות הוא לייבא את ExecutionContext שתפקידו לנהל את מאגר האשכולות. בלעדיו עתידנו לא יתנהל.

לאחר מכן, אנו מגדירים את "העבודה הגדולה" שלנו אשר פשוט מחכה לשנייה ומחזירה את התשומה שלה בתוספת אחת.

ואז יש לנו את חסימת ההבנה שלנו. במבנה זה, כל שורה בפנים מקצה את תוצאת המשרה לערך עם &lt; - אשר יהיה זמין לכל עתיד שלאחר מכן. סידרנו את העבודות שלנו כך שלמעט הראשון, כל אחד מקבל את התפוקה של העבודה הקודמת.

כמו כן, שים לב כי התוצאה של הבנה היא גם עתיד עם תפוקה הנקבעת על פי התשואה. לאחר הביצוע, התוצאה תהיה זמינה בפנים map. למטרתנו, אנו פשוט מכניסים את כל תפוקות העבודות לרשימה ולוקחים את גודלה.

בואו ננהל את זה.

אנו יכולים לראות את חמשת העתידיים מפוטרים בזה אחר זה. חשוב לציין כי יש להשתמש בהסדר זה רק כאשר העתיד תלוי בעתיד הקודם.

ריצה סימולטנית או מקבילה

אם העתיד אינו תלוי זה בזה, יש לפטר אותם במקביל. לצורך כך אנו נשתמש ב- Future.sequence . השם קצת מבלבל, אך באופן עקרוני הוא פשוט לוקח רשימה של עתיד והופך אותו לעתיד של רשימה. ההערכה, לעומת זאת, נעשית באופן אסינכרוני.

בואו ליצור דוגמה לעתיד מעורב רציף ומקביל.

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 לוקח רשימה של עתידים שאנו רוצים להפעיל בו זמנית. אז הנה לנו f2 ו- f4 המכילים שתי עבודות מקבילות. כטיעון המוזן בעתיד. תוצאה היא רשימה, התוצאה היא גם רשימה. ביישום ריאלי, ניתן לשלב את התוצאות לצורך חישוב נוסף. כאן ניקח את האלמנט הראשון מכל רשימה .headואז נעביר אותו ל- f3 ו- f5 בהתאמה.

בואו נראה את זה בפעולה:

אנו יכולים לראות את המשרות ב -2 ו -4 מפוטרות בו זמנית מה שמעיד על הקבלה מוצלחת. ראוי לציין כי לא תמיד מובטח ביצוע מקביל מכיוון שהוא תלוי בחוטים הזמינים. אם אין מספיק שרשורים אז רק חלק מהעבודות יפעלו במקביל. האחרים, לעומת זאת, יחכו עד שישתחררו עוד כמה שרשורים.

התאוששות משגיאות

סקאלה עתיד משלב שחזור המשמש כעתיד גיבוי כאשר מתרחשת שגיאה . זה מאפשר להרכב העתידי להסתיים גם עם כשלים. לשם המחשה, שקול קוד זה:

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

כמובן, זה לא יעבוד, מכיוון ש- "abc" אינו אינטי. עם התאוששות, אנו יכולים להציל אותו על ידי העברת ערך ברירת מחדל. בואו ננסה להעביר אפס:

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

כעת הקוד יפעל וייצור כזה כתוצאה מכך. בהרכב, אנו יכולים לכוונן כל עתיד כך כדי לוודא שהתהליך לא ייכשל.

עם זאת, ישנם גם מקרים בהם אנו רוצים לדחות שגיאות במפורש. למטרה זו, אנו יכולים להשתמש ב- Future.succesful ו- Future.f נכשלו כדי לאותת על תוצאת אימות. ואם לא אכפת לנו מכישלון אינדיבידואלי, אנו יכולים למקם את ההתאוששות כדי לתפוס כל שגיאה בתוך הרכב.

בואו נעבוד עוד קצת קוד באמצעות הבנה שבודקת אם הקלט הוא int תקף ונמוך מ 100. Future.failed ו- Future.successful הם שניהם עתידיים ולכן אנחנו לא צריכים לעטוף אותו באחד. Future.failed במיוחד דורש Throwable ולכן אנו הולכים ליצור אחד מותאם אישית עבור קלט גדול מ- 100. לאחר שכולנו את הכל יחד יהיו לנו כדלקמן:

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

שימו לב למיקום התאוששות. עם תצורה זו, הוא פשוט יירט כל שגיאה המתרחשת בתוך הבלוק. בואו לבדוק את זה בכמה קלטים שונים "5", "200" ו- "abc":

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

"5" הגיע לסוף אין בעיה. "200" ו- "abc" הגיעו להחלים. עכשיו, מה אם נרצה לטפל בכל שגיאה בנפרד? כאן נכנס לתמונה התאמת דפוסים. הרחבת גוש השחזור, יכול להיות לנו משהו כזה:

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

אולי ניחשתם נכון אבל תרחיש של הכל או כלום כזה משמש בדרך כלל בממשקי API ציבוריים. שירות כזה לא יעבד קלט לא חוקי, אך עליו להחזיר הודעה כדי ליידע את הלקוח מה הם עשו לא בסדר. על ידי הפרדת חריגים נוכל להעביר הודעה מותאמת אישית לכל שגיאה. אם אתה רוצה לבנות שירות כזה (עם מסגרת אינטרנט מהירה מאוד), עבור אל המאמר שלי ב- Vert.x.

העולם שמחוץ לסקאלה

דיברנו הרבה על כמה קל סקאלה עתיד. אבל האם זה באמת? כדי לענות עליה עלינו לבדוק כיצד זה נעשה בשפות אחרות. ניתן לטעון שהשפה הקרובה ביותר לסקאלה היא ג'אווה שכן שניהם פועלים ב- JVM. יתר על כן, ג'אווה 8 הציגה את ה- API של Concurrency עם CompletableFuture המסוגל גם לשרשר עתידיים. בואו ונעבד איתו את הדוגמא הראשונה.

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

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.