2017-07-29

שלום, AWS Lambda! (חלק ב')

בפוסט הקודם שלום, AWS Lambda! הצגנו את למבדה, ובעיקר כיצד ה main successful flow עובד.
לא הספקנו כמעט ולהיכנס לעומק / להתנהגויות השונות במודל של למבדה.

זה מה שננסה לעשות בפוסט הזה.


The Container Model


כאשר מתבצעת הפעלה (invocation) ראשונה של הפונקציה שלנו, למבדה מפעילה "Container" שבו תרוץ הפונקציה.
מהו ה container הזה? אין תיעוד מדויק, אך מדובר בסופו של דבר בסביבת ההרצה של הפונקציה, ע"פ ה settings שהגדרנו.

הרצת ה container אורכת זמן מסוים, נאמר כ 100-200ms - מה שנקרא cold start.
על הזמן הזה - המשתמש מחויב.
  • אם הפונקציה רצה עשר פעמים בדקה - אזי זו הפעלה אחת לכל גרסה / עותק חדש של פונקציה (הסבר בהמשך). הבדל זניח.
  • אם הפונקציה רצה פעם בעשר דקות, וזמן ה execution שלה קצר - ה cold start עשוי להכפיל את העלויות. 
הערה: העניין הכספי כנראה לא יגרום לכם לעזוב את השימוש בלמבדה, מטרת הדיון היא בעיקר להסביר את אופן העבודה של למבדה.

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

בכל הפעלה עוקבת של הפונקציה, למבדה תנסה לעשות שימוש חוזר בקונטיינר קיים. היא לא תעשה שימוש חוזר במקרים הבאים:
  • אין קונטיינר זמין - למשל "הרגו" אותו לא מזמן...
  • הבקשה הגיעה ל Availability Zone A, אבל הקונטיינר נמצא ב Availability Zone אחר.
  • יש מספר הרצות מקביל של הפונקציה: אם מספר ההרצות בשניה עלה מ 10 ל 100, למבדה תתחיל להריץ עוד ועוד קונטיינרים במקביל - עד שהצורך יסופק.
    כלומר: ייתכן ויש כבר 6 קונטיינרים עובדים, אך במקום לחכות שקונטיינר יתפנה - למבדה החליטה להפעיל קונטיינר שביעי.
    • בעת כתיבת הפוסט, אמזון לא תריץ יותר מ 1000 קונטיינרים במקביל - אלא אם ביקשתם.
    • למבדה תקבע את כמות המקביליות כתלות במקור ה event:
      • אם מדובר בהודעות ב Queue (או "stream-based-event") - המקביליות תהיה ע"פ כמות ה shards של queue.
      • כאשר מדובר בקריאות ישירות (למשל HTTP) - המקביליות תהיה מספר הקריאות בשניה * כמה שניות לוקח לפונקציה לרוץ בממוצע.
    • בכל מקרה, למבדה היא לא קסם. גם אם יש spike "מטורף" - אמזון לא תפתח יותר מ 500 קונטיינרים חדשים בדקה. אפשר לקרוא עוד פרטים על המדיניות כאן.
  • סיבה אחרת לא מתועדת / שאני לא מכיר.

זמני ה Cold Start תלויים ב:
  • כמובן: הקוד שלכם. אם אתם משתמשים ב ORM, או ספריות שדורשות אתחול משמעותי - הזמן הזה ישולם ב Cold Start.
  • כמות הזיכרון שהוקצתה לפונקציה (=> CPU). יותר זיכרון - אתחול מהיר יותר.
  • כמות הקוד שנטען ב ZIP, ספריות וכיוצ"ב: פחות קוד - אתחול מהיר יותר.
  • קוד ב node.js ופייטון יאותחל מהר יותר מקוד ג'אווה או #C.

זמני ההרצה של 4 עותקים זהים של פונקצית למבדה. הפונקציה memInfo0 נקראת בצורה תדירה - ולכן כמעט ואינה מושפעת מ cold start, לעומת הפונקציות האחרות שחוות cold start כמעט כל פעם: כלל שהפונקציה תדירה פחות - הסיכוי ל cold start גדל. מקור: New Relic.

למרות החשש האפשרי מ cold start על הביצועים - ההשפעה של ה cold start על משתמש הקצה היא לרוב קטנה למדי. הנה רכיבים טיפוסיים שגם להם אנו ממתינים בהפעלה של פונקציית למבדה:
  • network latency - עוד כ 100ms או יותר (?), אלא אם אתם קוראים לפונקציה באותו ה region - ואז ה latency הוא מילישניות בודדות.
  • זמני הריצה של ה API Gateway - כ 30 עד 150 מילישניות (ע"פ נתונים של New Relic).
    • הקמת חיבור מאובטח (TLS/SSL) - עשוי לארוך עוד 50-200 מילישניות.

דיי פופולארי היום לדבר על טכניקות ל"שמירת ה container חי" והימנעות מ Cold Start.
בשקלול של כל הרכיבים, נראה ש"הימנעות מ Cold Start" היא התעסקות דיי מיותרת - המשקל של הרכיבים האחרים הוא גדול יותר. לפחות, ברוב הגדול של המקרים.


Container reuse

אם לפונקציה שלכם יש קצב הפעלה בינוני עד גבוה, רבות מההפעלות יתרחשו על container קיים.
זה אומר ש:
  • הגדרות "סטטיות" שנעשו מחוץ ל handler function - עדיין תקפות: חיבור לבסיסי נתונים / רדיס וכו'.
  • Cache ששמרתם בזיכרון - עדיין מלא.
  • דברים שנרשמו לדיסק עדיין שם (יש לכם עד חצי ג'יגה אכסון בתיקיה tmp/)
זה פוטנציאל לשיפור ביצועים משמעותי.
כמובן, שלעולם אי אפשר להסתמך על container reuse: אם הוא יצא - אפשר לנצל אותו, אם לא אז לא. עליכם לכתוב את הקוד בהתאם.

כמו כן, חשוב להבין את התנהגות ה cache בצורה נכונה: אם יש מקביליות גבוהה (concurrent invocation), אזי יש כמה  containers שכל אחד עם cache ב state שונה. חשוב לקחת את זה בחשבון. אם חשוב לכם cache אחיד - פשוט השתמשו ברדיס.



Throttling: a process responsible for regulating the rate at which application processing is conducted
מקור: Robyn


Throttling and retries


כאשר יש הרבה יותר בקשות הרצה להפעלה של פונקציות מאשר containers - למבדה תגדיל את מספר ה containers הרצים.
כאשר יש מעט יותר בקשות הרצה להפעלה של פונקציות מאשר containers - למבדה תבצע throttling. כלומר: תמתין עם הבקשה עוד קצת - ואז תפעיל אותה על גבי ה containers הקיימים.
  • עבור מקורות מסוג Queue (רשמית: "stream-based events") - למבדה תבצע retry לעד. עד שהבקשות מטופלות.
  • עבור מקור קריאה אסינכרונית - למבדה תבצע retry בטווח של עד 6 שעות מרגע הקריאה, עם המתנה בין ניסיון לניסיון, מה שאמור ליצר סיכוי נמוך מאוד למצב של הודעה שלא טופלה.
  • עבור קריאות סינכרוניות (ה event source ממתין לתשובה) - למבדה תחזיר לאפליקציה HTTP status code של 429 - ותצפה שהאפליקציה תבצע retry בעצמה.
Throttling הוא מצב תקין וצפוי בלמבדה - מצב שעליכם לקחת בחשבון! הוא יקרה.

פונקציית הלמבדה שלכם כמובן יכולה להחזיר שגיאות. כאלו שחזרו מלבדה עצמה או מהקוד שלכם.
בלמבדה יש הפרדה בין 2 סוגי Exceptions:
  • Synchronous Exception - כזו שתוצאתה חוזרת למי שקרא לפונקציה.
  • Asynchronous Exception - כזו שתוצאתה מגיעה ל cloudwatch. לקוח הפונקציה לא מודע לדבר.

כשאר הפעלה של פונקציה נתקלה ב Synchronous Exception (שנגרמה ע"י הקוד שלכם או ע"י למבדה) - למבדה תבצע retry ע"פ המדיניות הבאה:
  • עבור מקורות מסוג Queue (רשמית: "stream-based events") - למבדה תבצע retry כל עוד ה data מבחינת ה queue הוא בתוקף.
  • עבור מקור קריאה אסינכרונית - למבדה תבצע retry עוד שתי פעמים, בד"כ בהפרש של כדקה, ואז תשלח את ההודעה ל Dead Letter Queue שהוגדר על ידכם על גבי SNS או SQA.
    • ליתר דיוק בדיקות הראו שאכן ב 99% מהפעמים יהיו 2 retries, אבל לעתים רחוקות יהיה רק retry אחד - ולפעמים גם יבוצעו עד שש retries. מקור.
  • עבור קריאות סינכרוניות (ה event source ממתין לתשובה) - הודעת השגיאה תחזור לאפליקציה, ובאחריותה לבצע retries.


חשוב לציין מגבלה שלא כתובה פה: בעת כתיבת הפוסט, ל API Gateway יש timeout קבוע של 30 שניות. אם יש לכם פונקציה שזמן הריצה שלה ארוך יותר - API Gateway יבצע timeout, בכל זאת.


בשלב הזה אתם אמורים להבין כמעט את כל המדדים שמסופקים ב Monitoring של למבדה:


המדד היחידי שלא הוסבר הוא ה iterator age שרלוונטי רק למקורות מסוג stream-based events.
הוא מציין כמה זמן ארך מרגע שפונקציית הלמבדה שלנו קיבלה batch של הודעות, ועד שסיימה לטפל בהודעה האחרונה. כלומר: מה ה latency המרבי של טיפול בהודעות ב batch, שהגיעו ממקור שכזה.


Lambda@Edge


שווה להזכיר את שירות ה Lambda@Edge ששוחרר לאחרונה, שירות המאפשר להריץ פונקציות למבדה (Nodejs בלבד, כרגע) על ה PoPs (כלומר: Point of Presence) של CloudFront.

לאמזון יש כ 14 regions בעולם עליהם אפשר להריץ את כל שירותי ה AWS, אבל יש לה קרוב ל 100 מיקומים בהם יש לה נוכחות - עבור שירות ה CDN שלה, הרי הוא CloudFront.

עבור פעולות פשוטות למדי, אם תוכלו להריץ את הקוד קרוב מאוד ללקוח שלכם - תוכלו לספק לו תשובה מהירה ביותר: ה latency ל"קוד" שלכם עשוי להיות עשרות מילי-שניות, ולא מאות מילי-שינות.
למבדה_על_הקצה (Lambda@Edge) תנהל עבורכם את שכפול הקוד לרחבי העולם - ותדאג שהלקוחות שלכם, יקבלו זמני תגובה משופרים.

את פונקציית הלמבדה_על_הקצה שלכם, אפשר לחבר לארבע נקודות-חיבור שונות בתהליך של cloudfront (בתרשים: החצים הירוקים והכתומים), ולבצע שינוי על המשאב שהתבקש.

מקור: אמזון


כמובן שבשירות כזה מאוד חשוב לספק זמני תגובה מהירים מאוד - מה שלא תמיד מתרחש בצמד למבדה + API Gateway.

ה API Gateway הוא מחוץ לתמונה, יצירת ה HTTP connection עם TLS/SSL - כבר מתבצע ע"י cloudfront, ולכן הרכיב היחידי שעלול לגרום לעיכובים (ולייתר את היתרון שבריצה על PoP) הוא זמן האתחול של ה container של למבדה. לא סתם התחילו ב nodejs כסביבת-ריצה.

ללמבדה_על_הקצה - יש מגבלות משלה (מקסימום 128MB זיכרון, זמני ריצה קצרים יותר, אין גישה לשירותים אחרים של אמזון, וכו')


שירותים נוסח למבדה_על_הקצה כבר היו קיימים (למשל: PubNub Blocks) - אבל לאמזון יש יתרון מובנה מול מתחרים קטנים יותר.

יהיה מעניין!



סיכום


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

שיהיה בהצלחה!




----

לינקים רלוונטיים

Lambda@Edge

2017-07-27

קוטלין (Kotlin) למפתחי ג'אווה ותיקים - חלק ג': מחלקות

פוסט זה הוא המשך של:
קוטלין (Kotlin) למפתחי ג'אווה ותיקים - חלק א': הבסיס
קוטלין (Kotlin) למפתחי ג'אווה ותיקים - חלק ב': פונקציות


הגדרת מחלקה פשוטה


בואו נתחיל עם המחלקה הפשוטה ביותר האפשרית בקטולין:


פשוט, לא?
האמת שזה דיי דומה למחלקה ריקה בג'אווה: פשוט אם אין body - אז אין צורך בסוגריים מסולסלים.

מה השימוש במחלקה ריקה?


לא הרבה.
אפשר לייצר ככה custom exception או Marker interface או Marker Annotation (בתחביר מקוצר הרבה מג'אווה).


טוב, עכשיו נתחיל יותר ברצינות:


  1. בקוטלין לא ניתן להגדיר שדות (fields) למחלקה. במקום זאת - מגדירים תכונות (properties). מה זה אומר?
    1. השורה שלנו תגרום לקומפיילר לייצר גם שדה, וגם accessor methods לשדה הזה.
      1. כאשר כותבים  בקוטלין - הגישה לתכונה (property) תעשה בעזרת person.age - כאילו זהו field שהוא פשוט public.
      2. כאשר כותבים בג'אווה - הגישה לתכונה של מחלקה הכתובה בקוטלין תעשה בעזרת ()person.getAge  ו ()person.setAge - כמקובל.
    2. מאחורי כל תכונה (property) באמת קיים שדה (field). לעתים - לא יהיה בשדה הזה בכלל שימוש. נראה דוגמה בהמשך.
    3. הסיבה מאחורי הוספת properties לשפה, היא כנראה Item 14 בספר "Effective Java" - המזהיר בפני חשיפת שדות שאז לא יהיה ניתן לשנות, לבקר, ולהגביל את הגישה אליהם. "אחרי שנחשפו - זהו!... התלות קיימת וזו יכולה להיות עבודה קשה להסיר אותה..."
    4. בקוטלין נתנו לנו את האפשרות להוסיף custom getter/setter בכל יום שנצטרך - אבל בלי הסרבול של לכתוב getters / setters בעצמנו, בכל פעם, כי "אולי בעתיד יהיה צורך".
      להיות עם - ולהרגיש בלי.
  2. בקוטלין יש primary constructor, וייתכן שיהיו secondary constructors.
    ה primary constructor מוגדר באותה שורה עם הגדרת המחלקה.
    1. מגדירים אותו בעזרת המילה השמורה constructor.
    2. מגדירים אלו פרמטרים הוא יקבל.
    3. אם לא מגדירים primary constructor אזי נוצר default primary constructor - ללא פרמטרים.
  3. ה init block נקרא בכל יצירה של instance של המחלקה, והוא בעצם משמש כגוף הפונקציה של הבנאי הראשי (primary constructor) - אם צריך כזה.
    1. כל התכונות של המחלקה צריכות להיות מאותחלות. אם נסיר את השורה "age = 0" - נקבל שגיאת קומפילציה.
      הקומפיילר דורש שאנו נאתחל את התכונות באותה השורה (מה שנקרא initializer, כמו ב name) - או שנאתחל אותם ב init block / בבנאי הראשי.
  4. הנה הגדרה של בנאי משני למחלקה.
    1. ההגדרה שלו נעשית בעזרת המילה השמורה constructor.
    2. מגדירים את הפרמטרים של הבנאי המשני.
    3. חובה להפעיל את הבנאי הראשי: 
      1. או ישירות - ע"י קריאה עם הפרמטרים המתאימים.
      2. או ע"י קריאה לבנאי משני אחר - שהוא יפעיל את הבנאי הראשי.
      3. כאשר קיים default primary constructor - אין צורך ב ()this :.
  5. הנה יצירת מופע של המחלקה בעזרת שימוש בבנאי הראשי.
  6. הנה יצירת מופע של המחלקה - בעזרת שימוש בבנאי המשני.
  7. הנה קריאה לתכונה - היא באמת נראית כמו קריאה לשדה בג'אווה - מה שהופך את הקוד למעט יותר "נקי בעין".

יש לא מעט מידע בפסקה הזו - אולי תרצו לעבור עליה שוב.
אתם בוודאי רואים שיש פה השפעה לא מעטה #C וגם קצת מסקאלה.


האמת, שיש דרך פשוטה יותר לכתוב את המחלקה Person. הנה היא לפניכם:


  1. הצלחנו לפשט את המחלקה דרמטית, בעזרת הגדרה "נבונה" יותר של הבנאי הראשי.
    1. המילה constructor בשורת המחלקה, לתיאור הבנאי הראשי - היא אופציונלית, ואפשר לוותר עליה.
    2. אם מגדירים val או var על הפרמטרים של הבנאי הראשי - הרי זה שקול להגדרת תכונות בגוף המחלקה. הארגומנטים שנשלחו לבנאי - יאתחלו את התכונות הללו. קצר ונוח.
      יכולת זו שמורה לבנאי הראשי בלבד.
    3. השימוש ב default value בחתימה של הבנאי הראשי - ייתרה את הצורך בבנאי משני.
      1. במקרים לא מעטים, השימוש ב default values - יכול לחסוך שימוש ב Builder Pattern, ולחסוך לא-מעט קוד.
  2. הנה ההרצה: אין שום שינוי, כי לא היה שום שינוי. הקוד שקול לחלוטין.

אמנם כך עדיף לכתוב את המחלקה, אך היה חשוב לי לעבור בדרך הארוכה - עבור הלמידה.


עוד קצת על תכונות (Properties)


בואו נחזור ונחדד עוד כמה דברים לגבי תכונות:



  1. הגדרנו למחלקה תכונה בשם age - ודרסנו את ה getter. ממש דומה לתחביר של #C.
    1. כעקרון, כאשר דורסים רק שלפן (accessor) אחד, במקרה שלנו: getter, השלפן השני - עדיין קיים במימוש ברירת המחדל שלו. כלומר: ל age עדיין יש default setter.
    2. ספציפית במקרה שלנו, מכיוון ש age הוגדר כ val (כלומר "immutable") - לא ייווצר setter.
  2. רק לצורך ההדגמה יצרתי תכונה מאוד דומה, אותה הגדרנו כ var. במקרה כזה, אנחנו מחויבים לאתחל את השדה של התכונה - גם אם לא יעשה בו שימוש לעולם (במקרה הזה: עם הערך אפס).
  3. כאשר ה getter שלי הוא פשוט - אני יכול להשתמש בתחביר shorthand, בעזרת הסימן =.
    1. יכולתי להשתמש בתחביר shorthand גם עבור age.
  4. אם אני לא רוצה שיגשו ל setter של התכונה - אני פשוט אגדיר אותה כ private.
    התחביר הזה אומר לקומפיילר: צור default setter - אך עשה אותו private.
  5. הנה - סתם בשביל ההדגמה: אני יכול להשתמש ב setter של התכונה בתוך המחלקה.
  6. אגדיר תכונה נוספת בשם nickname - אין לי בעיה לאתחל אותה בערך שנגזר מתכונה אחרת - name.
  7. כאן מימשתי setter לדוגמה. ה getter ה default-י עדיין קיים וזמין.
  8. מה קורה כאשר אנחנו רוצים, ב customer accessor שלנו לגשת לשדה שמאחורי התכונה?
    1. עושים את זה בעזרת המילה field.
    2. field היא מה שנקרא soft keyword, כלומר: keyword שקיים רק ב context מסוים. ממש כמו it - שראינו בפוסט הקודם.
  9. בניסיון ההרצה, שלושת השורות הללו יגרמו ל compilation error.
    1. את לשלושת התכונות הללו: age, name, ו ageNextYear - לא ניתן לקבוע ערך מחוץ למחלקה.

הנה תוצאת ההרצה:




Visibility Modifiers


בקוטלין, visibility ברירת המחדל היא תמיד public. יש שוני קטן בהגדרות כאשר ה visibility modifier מוגדר:
  • בתוך מחלקה.
  • ב top level, כלומר: עבור הגדרה של מחלקות, משתנים, או פונקציות שאינן שייכות למחלקה.

הנה ההגדרות:
  • private 
    • יתנהג כמו ג'אווה בתוך מחלקה
    • יגביל גישה לאותו קובץ בלבד, אם השתמשו בו ב top-level.
  • protected
    • יתנהג כמו ג'אווה בתוך מחלקה
    • לא ניתן להשתמש בו ב top-level.
  • internal
    • היא נראות חדשה לקוטלין, שמגדירה אפשרות לגשת לכל מי שנמצא באותו המודול.
      • כיצד מוגדר מודול? ע"י תהליך הקומפליציה: קבצים שקומפלו ביחד.
        זה יכול להיות מודול של Maven, של Gradle, או מודול ב IntelliJ - למשל.
    • היא מתנהגת אותו הדבר בתוך המחלקה, וב top-level


בואו נראה קצת קוד:


  1. הגדרתי את המשתנה כ private - לא יוכלו לגשת אליו מקובץ אחר.
  2. הגדרתי מחלקה שהיא internal - זמינה רק בתוך ה module.
    1. כל המחלקות בקוטלין הן final - לא ניתן לרשת מהן, אלא אם מרשים זאת במפורש בעזרת השימוש ב open. אהבתי!
  3. מה עושים כאשר רוצים להגדיר "שדה פנימי" בקוטלין? רק לשימוש המחלקה?
    - מגדירים תכונה שהיא private, ומשתמשים בה.
  4. פה נראה שאולי עשיתי איזה טריק: הגדרתי getter ו setter על התוכנה - כך שאי אפשר לגשת ל accessors שלה. האם הפכתי אותה ל private?
    1. בפועל: אין תחביר כזה:
    2. הקומפיילר מתעלם מ get ומגדיר רק את ה default setter כ private.
    3. תראו למטה - אמנם הקוד מתקמפל, אבל אני בהחלט יכול לגשת ל someHiddenField ממחלקה אחרת. אופס!
  5. הנה דוגמה תקינה ל default setter שהוא private. אפשר כמובן גם להגדיר custom setter באותו האופן.
  6. ניסיתי להגדיר internal getter - אך קיבלתי שגיאת קומפילציה: ה getter של תכונות חייב להיות באותה נראות כמו התכונה עצמה. חשבו אילו בעיות יכלו להיות אם לא...
  7. באופן הבא אני יכול להגדיר נראות (private) על primary constructor.
    כאשר אני משתמש ב visibility modifier על הבנאי - אני חייב להשתמש במילה constructor.
  8. על השורה הזו אני מקבל warning בקומפליציה: ה class לא הוגדר כ open - אז לא ניתן לרשת ממנו. אין טעם או משמעות להגדיר נראות של protected. צודק הקומפיילר.

נמשיך הלאה, ל"סוגים מיוחדים" של מחלקות בקוטלין.



Data Class


אחד הבזבוזים הגדולים של boilerplate code בג'אווה הוא ביצירה של data classes - מחלקות שכל תפקידן להחזיק כמות מסוימת של נתונים.

אני תמיד הייתי משתמש ב public fields, אבל יש כאלו שהקפידו על getters ו setters, וגם hashCode ו equals וכו'... הרבה קוד - עבור משהו מאוד סטנדרטי.

בקוטלין אפשר להגדיר data class, מחלקה שמספקת לנו:
  • תיאור כוונות ברור: המילה data מצביעה בבירור על הכוונה מאחורי המחלקה.
  • מימוש ל ()equals(), hashCode(), clone, ומימוש default-י וסביר לחלוטין של ()toString.
    בערך כל מה שמחלקה כזו צריכה.


  1. כך מגדירים data class, שהיא מחלקה לכל דבר. את התכונות של ה dataclass הגדרתי בתוך הבנאי הראשי - כמו שהראנו קודם. זה הכי נוח.
  2. מכיוון שזו מחלקה לכל דבר, אני יכול להגדיר לה member function. כמובן שבד"כ לא יהיו פונקציות ל data class.
  3. אמנם קיבלתי מימוש סטנדרטי של כמה מתודות, אבל אם צריך - אני יכול לדרוס אותן.
    בקוטלין override היא לא optional annotation, אלא mandatory keyword. טוב מאוד!
  4. הנה אני עושה השוואה בין אובייקטים של ה data class שלי. הם לא שווים, כי המימוש שמסופק ל equals - משווה את כל התכונות של המחלקה.
  5. לאחר שעשיתי השמה (שימוש ב ()clone) - האובייקטים שווים. זו אכן ההתנהגות הצפויה מ data class.

שני Data Classes שימושיים שמסופקים כחלק מהשפה הם Pair ו Triple, המאפשרים לנו להעביר זוגות או שלשות של פרמטרים.


אלו באמת רק Data Classes. למשל, הנה המימוש של Triple:




Enum Class


בדומה לג'אווה, לקוטלין יש enum... class. בואו נראה כיצד הוא נראה:


  1. הנה דוגמה ל enum פשוט. כל איבר הוא בעצם אובייקט - מופע של המחלקה.
    1. זהו אחד המקרים המעטים בהם צריך בקוטלין לכתוב יותר קוד מאשר בג'אווה: "enum class" במקום "enum"  😏
    2. מכיוון שהאיברים הם אובייקטים, אני יכול להרחיב אותם, למשל: לדרוס פונקציה של המחלקה (או להוסיף פונקציה חדשה). הפונקציות הללו שייכות לאובייקט, ולא למחלקה!
  2. אני יכול הוסיף גם פונקציה למחלקה, שתהיה זמינה לכל אחד מהאובייקטים ב enum.
  3. יש מקרה דיי נדיר בו עלי להשתמש בנקודה-פסיק בקוטלין: 
    1. אם הוספתי ל enum class פונקציות, הקומפיילר יבקש עזרה לדעת היכן נגמרה רשימת האיברים, והיכן מתחילה המחלקה. ההפרדה מסומנת בעזרת נקודה-פסיק.
    2. שימו לב שב TriColor לא היינו צריכים להוסיף נקודה-פסיק (אבל יכולנו - זה תקין).
  4. מכיוון ש BLUE הוא אובייקט - כאן תופעל, באופן טבעי, הפונקציה ()toString.
  5. לכל enum class יש את הפונקציה ()valueOf, המחפשת איבר ע"פ השם שלו. 
    1. ניתן להציץ בקוד המקור של ה Abstract Enum Class כאן.
  6. לכל אובייקט ב enum יש 2 תכונות מובנות: 
    1. name - (ששווה לשם האיבר), בחוסר תלות מ ()toString.
    2. ordinal - שהוא האינדקס של האיבר.
  7. תכונה אחרונה חשובה של enum היא הפונקציה ()values - המחזירה מערך של האיברים. לצורך הפלט - המרתי את המערך לרשימה.
  8. אני יכול לשנות את המחלקה ולהוסיף לה תכונות דרך הבנאי הראשי.
    הנה ל TwoColor הוספתי תכונה בשם value - שיש לכל אחד מהאיברים שלה.
הנה הפלט:




סיכום


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


שיהיה בהצלחה!





2017-07-25

שלום, AWS Lambda!

למבדה, שירות ה FaaS של AWS, הוא שירות המאפשר להריץ "פונקציות" בצורה מנוהלת:
אין צורך לחשוב על שרתים, על תשתיות, High Availability ו Fault tolerance, או על Scaling.

אתם כותבים את הפונקציה, ומוסיפים קצת הגדרות.
שירות למבדה יעשה עבורכם את הניהול: הקצאת CPU, רשת ו I/O, זמינות, טיפול ב scale, וכו'.

ללמבדה יש גם גם כמה נקודות שכדאי להיזהר איתן:
  • למבדה יכולה לחסוך המון כאשר יש מעט פונקציות מנוהלות. כאשר המספר גדל - הניהול הופך לקשה. 
  • התיעוד של למבדה הוא עדיין חסר: גם ע"י אמזון, וגם באינרנט בכלל. עדיין יש התנהגויות שלא פורסמו במדויק ע"י אמזון (אך נצפו ע"י הקהילה). 
  • ניטור ו troubleshooting בלמבדה הוא עדיין מאתגר, אם כי יש שיפור תמידי, למשל: 
  • מדי פעם, ייתכן כשל בשירות הלמבדה, מה שעלול למנוע מהפונקציה שלכם לרוץ למספר דקות. נראה שהמצב משתפר עם הזמן.

בעת כתיבת הפוסט, השירות תומך בסביבות הריצה של Java, Node.js, Python, ו #C.
עם מעט "tweaking", ניתן להריץ שפות נוספות כמו Go על גבי סביבות הריצה הקיימות.

למבדה עדיין נחשב שירות צעיר, אם כי ישנם כבר מספר frameworks המנסים להקל על השימוש בו: הפופולארים ביניהם הם כנראה Serverless ו Apex.



מקור: אמזון




שלום, למבדה!



הנה פונקציית ה Hello World בלמבדה. היא מאוד פשוטה.

ה event הוא אובייקט ה input שלנו, הוא יכיל את כל המידע שזמין להפעלת פונקציית הלמבדה. פונקציית למבדה היא מעין "טריגר ל event" - ועל כן המינוח.
בג'אווה וב #C יש וריאציה בה ניתן לקבל את ה input כ stream.

ה context הוא אובייקט בעזרתו אנחנו מתקשרים עם סביבת הריצה של למבדה, למשל: לבדוק כמה זמן ריצה נותר לנו (()context.getRemainingTimeInMillis) או כדי לקבוע את תוצאת הרצת הפונקציה (succeed או fail).

ה callback ספציפי למימוש של Node.js - ולו קוראים כאשר סיימנו את הפעולה (אם אנחנו רוצים לשדר פלט).
הפרמטר הראשון הוא error (אם יש), השני הוא אובייקט התוצאה, שיעבור ()JSON.stringify.

מדוע בחרתי ב node.js עבור הדוגמה?
זו הסביבה הכי קלה לשימוש והכי מתועדת של למבדה. ג'אווהסקריפט היא קלילה (זמן טעינה מהיר) ואינה צורכת הרבה זיכרון - מה שטוב עבור פונקציות למבדה. גם פייטון נמצאת במקום טוב, אבל התיעוד על Java ו #C - נמצא מאחור.

מה עושים עם הקוד? "איפה שמים אותו"?
בואו נראה קצת מסכים (מה AWS Console) - ה UI עוזר לשקף דברים שלא נראה ב aws-cli:


הלכתי ב AWS Console (קרי - ה UI) לאזור של למבדה, ויצרתי פונקציה חדשה.
אני אבחר את ה template הריק. באופן כללי אני לא אוהב להסתמך על templates...



טריגרים? - לא מעניינים אותי. אני רוצה רק קוד! נקסט!


  1. שם הפונקציה, כפי שיופיע ב console ובכלי האדמיניסטרציה.
  2. כאן בוחרים סביבת-ריצה. לסביבות השונות יש defaults שונים וגם פרמטרים שונים לקונפיגורציה.
  3. יש אופציה להקליד את קוד הפונקציה בתוך online editor - אבל בסביבה אמיתית הרבה יותר הגיוני לטעון את הקוד ל S3 - ולהשתמש בו משם. ממש path לשם הקובץ.


הנה עוד הגדרות, והן קצת יותר מעניינות:
  1. אמנם כמות הזיכרון המוקצה לפונקציה היא תחת "Advanced setting" - אך יש לה חשיבות גדולה.
    אמזון לא מכירה את הפונקציה שלנו ולא לוקחת "אחריות" על הקצאת הזיכרון. זו אחריות שלנו.
    1. ללמבדה יש monitoring שיראו לנו אם הפונקציה שלנו מתקרבת לקצה הזיכרון שמוקצה לה (או כשלה בשל מחזור בזיכרון) - ואז אפשר להגדיל את נפח הזיכרון. המקסימום הוא 1.5GB.
    2. החיוב בלמבדה נעשה ע"פ GB-second שנצרכו ע"י הפונקציה, כאשר אמזון מעגלת את זמן ריצת הפונקציה לרזולוציה של 100ms כלפי מעלה. פונקציה שרצה במשך 6ms - תחויב בעשירית GB-second על ההרצה הספציפית.
    3. למבדה תקצה CPU לפונקציה ע"פ הזיכרון שהוגדר. לא מפורט כמה CPU זה יהיה, אך יש קשר ישיר בין כמות הזיכרון שהוקצתה - ל CPU שיוקצה: על מכונה עם 4GB זכרון, פונקציה שהוקצה לה 1GB זיכרון - תקבל 25% מה CPU time - יחס ישר. המודל הזה מפשט לאמזון את ה scheduling של invocation של פונקציות - לחומרה הזמינה. הגיוני.
      1. המשמעות הראשונה היא שאם יש לי פונקציה שזקוקה להרבה CPU, ומעט זיכרון - יהיה עלי להקצות עבורה הרבה זיכרון בכדי לקבל חלק גדול יותר מה CPU.
        בחישובי עלות, פעמים רבות יהיה יותר זול להקצות לפונקציה הרעבה ל CPU יותר זיכרון/CPU. היא תרוץ מהר יותר - וכף נחוייב על פחות GB-sec.
      2. למרות שאין תיעוד ברור בנושא, נראה שמעל הקצאה של 1GB - ייתכן ופונקציית הלמבדה תקבל הקצאה של שני cpu cores. אין מניעה להשתמש ב threads בתוך פונקציית למבדה.
    4. מעניין לציין, שה default ב UI של nodeJs הוא 128MB זיכרון, בעוד זה של ג'אווה הוא 512MB זיכרון. לא קשה להגיע בג'אווה לצריכה גבוהה של זיכרון - בשל tradeoff תכנוני שנלקח ע"י מתכנני ג'אווה וה JVM. אמזון לא רוצה להכשיל אתכם - ולכן מקצה ב default גבוה מספיק עבור רוב הפונקציות האפשריות.
  2. עלי גם להגדיר timeout להרצת הפונקציה. ישנן פונקציות ש invoication שלהן עשוי לארוך דקה או שתיים - וזה בסדר. בפונקציה כמו שלי ("שלום עולם") - זמן ריצה של דקה משמעו באג שגרם ל infinite loop, שיגרום לי לשלם הרבה יותר ממה שהתכוונתי. האחריות על ה tradeoff בין סיכון לקטיעת הפונקציה ובין סיכון לתשלום מוגזם - היא על המשתמש (אבל ה default הוא הגיוני).
    1. לא ניתן כיום לקבוע timeout ארוך מ 5 דקות לפונקציה. אם יש לכם עיבוד כבד (נאמר: ETL) - אזי יהיה עליכם לשבור את העבודה ל chunks קטנים יותר. זה בריא גם בשבילכם מהסיבות הנ"ל.
  3. כאן אני מצביע על נקודת ההרצה של הפונקציה (ה "main" שלה).
    במקרה של nodejs, אם שם הקובץ שלי הוא index.js, ועשיתי export לפונצקיה בשם handler אזי עלי להקליד "index.handler". לכל סביבת ריצה יש חוקים מעט שונים.
  4. אלו הן הרשאות הריצה (execution) של הפונקציה - הרשאות לגשת לשירותים אחרים של AWS.
    יצרתי role עם הרשאות ה default (קרי basic execution policy - גישה לכתיבת לוגים וזהו) - וקראתי לו lambda_basic_execution - שאוכל להשתמש בו בפונקציות נוספות.


אני ממשיך הלאה, מאשר את יצירת הפונקציה, ומגיע למסך ניהול הפונקציה:


הוספתי שורת קוד של logging בכדי להדגים את כלי האדמיניסטרציה של למבדה. היכולת לכתוב לוגים היא חשובה מאוד - זו הדרך הכי יעילה לבדוק איך הפונקציה רצה. ב nodeJS פעולת logging נעשית באמצעות console.log (הרי "קל" לדרוס את console). בג'אווה, למשל, מקבלים את ה Logger מתוך ה context.

אני יכול ללחוץ על פתור "Test" - ולראות את הפונקציה שלנו בפעולה!

  1. הנה ה output של ריצת-הבדיקה. יש פלט - זה כבר טוב! אחרת הייתה לי הודעת שגיאה.
  2. הנה הלוג של ריצת-הבדיקה - אני יכול לראות שם את הלוג שכתבתי.
  3. מאחר ולא הרצתי את הפונקציה לאחרונה, אנו רואים ביצועים של cold state. בהרצות הבאות שאריץ בטווח של דקות - הביצוע ייקח פחות מ 1ms (זמן הביצוע הוא רק של הקוד שלי, ולא כולל את החלק של למבדה). ה cold start - הוא נושא לפוסט המשך.
  4. הנה אפשר לראות את צריכת הזכרון של ההרצה. בהרצה עוקבת הזיכרון מעט יותר גבוה: 22-24MB. מדוע? - פוסט המשך. 

כמובן, שברגע שאני רוצה שהפונקציה שלי תהיה reproducible (ליצור אותה קודם על סביבת בדיקות ורק אז ליצור בדיוק אותו דבר בסביבת הפרודקשיין), אני לא אשתמש ב UI אלא בכלי ניהול תצורה כמו Terraform בכדי לייצר אותה.



למבדה Prison Break


איזה יופי! איזו גאווה! כתבנו פונצקיית למבדה שרצה. אשכרה רצה!
אני לוחץ שוב, ושוב על כפתור ה "Test" - ואני מרוצה.

אבל נניח שאני רוצה לקרוא לה ממקור שאינו ה UI של אמזון. כרגע, זה לא אפשרי...

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

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




לפונקציות למבדה יש שני סוגי הרשאות:
  • הרשאות ריצה (Execution) - הרשאות לגשת לשירותים אחרים של AWS. 
    • את ההרשאות האלו מגדירים כ IAM role - כמו "lambda_basic_execution" שהגדרנו למעלה.
    • בתיעוד של למבדה קוראים לחלק הזה גם בשם Pull Model - מה הפונקציה שלכם יכולה "למשוך".
    • לפעמים הטריגר של הפונקציה יהיה רק scheduler ("רוצי כל 5 דקות") ואז נפונקציה תקרא ל SQS או Kinesis - למשוך משם נתונים ולטפל בהם.
  • הרשאות הפעלה (Invocation) - הרשאות הנדרשות ע"י ה trigger / event source - על מנת להפעיל את פונקצית הלמבדה.
    • את ההרשאות מגדירים גם כ IAM role - אבל על מי שרוצה לקרוא ללמבדה, למשל: API ב API Gateway.
    • בתיעוד של למבדה קוראים לחלק הזה גם בשם Push Model - מי מפעיל את הפונקציה שלכם.

נגדיר טריגר מסוג AWS API Gateway.


כמובן שעדיף להגדיר IAM role, אבל כרגע לצורך הדוגמה - אני בוחר ב "Open": כל מי שיש לו את ה URL יכול להפעיל את הפונקציה (ולחייב אותי).



סיימנו. תוכנית הבריחה הושלמה!

אני לוחץ על קישור 1 - שהוא ה URL של ה API שיצרתי:



אופס! ב Prison Break, כמו ב Prison Break - תוכניות משתבשות. צריך לעבוד על עוד משהו.
אני בודק שוב: הפונקציה שלי תקינה.

אז מה הבעיה?

בלמבדה זו שאלה שתשאלו את עצמכם לא מעט. כלי העבודה העיקרי - הוא לוגים.

לכן אני אלחץ על קישור 2 - שלוקח אותי ל view הכללי  של ה API שהגדרתי ב API Gateway. משמעות המונח "ANY" = כל מתודות ה HTTP.


המסך הזה ממש "מצייר" את ה flow שמתרחש: מהלקוח, דרך ה API Gateway (קודם צד ה HTTP, ואז צד ה Integration / Transformation), עד ללמבדה - ובחזרה.

אני יכול להקליד על 1 לראות את מהות האינטגרציה. מכיוון שה Integration type הוא lambda_proxy - אזי כמעט ואין מה להגדיר. כמעט הכל מנוהל ע"י אמזון. כנ"ל לגבי הטרנפורמציה בחזרה.
קישור 2 - מוביל אותי חזרה למסך של ניהול פונקציית הלמבדה שלי.
אני לוחץ על קישור 3 - שמאפשר לי לבדוק, מקצה לקצה, את ה API ביחד עם פונקציית הלמבדה.
שם, בתוך הלוג - אני מוצא את הבעיה:


לפני הטרנספורמציה הכל נראה בסדר.
שניה! איזו טרנספורמציה?

קריאת ה HTTP עצמה לא מתאימה בדיוק לאובייקט ה event שאנחנו מקבלים. מישהו לקח את ה HTTP payload (טקסט רציף), פרסר אותו וסידר אותו יפה במבנה היררכי של פרמטרים. מי שעשה את זה הוא ה lambada_proxy של ה API gateway: הוא המיר את ה HTTP למבנה json שלמבדה יודעת לקבל ולעטוף באובייקט ה event.

אותה הדרך חזרה: ה lambda-proxy מצפה לפורמט מסויים של תשובה מלמבדה. כל צרכן - והציפיות שלו.
באופן כללי ההגדרה היא שהבקשה והתשובה של פונקיית למבדה הן בפורמט JSON. אפילו את זה לא עשינו.


הנה, שינינו את הקוד למבנה המצופה ע"י ה lamda_proxy והנה התוצאה:



איזה כיף!



סיכום



"ליאור, למה כל הסיבוב הזה? למה לא כתבת את הקוד הנכון מלכתחילה? גם ככה הפוסטים שלך ארוכים!!"

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

מקווה שהצלחנו.

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


עדכון: הנה חלק ב' של הפוסט.



שיהיה בהצלחה!




רפרנסים

מדריך למפתח של אמזון על פונקציות למבדה
AWS Lambda Blog



2017-07-23

רברסים 2017

כנס רברסים 2017 מגיע עלינו לטובה.
לפני כמה ימים נסתיים שלב הגשת ההצעות - והשבוע מצביעים על התוכן שיוצג.
הכנס יתקיים השנה במכללה למנהל שבראשון לציון.

אם אתם לא מכירים את כנס רברסים - חפשו בגוגל...
לדעתי זה הכנס הכי משמעותי בעולם התוכנה שמתקיים בישראל.
אני חושב שחוץ מאשר הגימיקים (שזה שטויות) - הוא משתווה לכנסים הטובים ברמה הבין לאומית דוגמת QCON ו/או ;GOTO.




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

אני הצעתי סשן על Effective Software Design (או ESD) - שיתבסס על פוסט באותו נושא שפרסמתי לא מזמן.
נתקלתי בעוד 20~ הצעות של אנשים שאני מכיר - שזה תמיד נחמד!


הצביעו לסשנים שנראה לכם שתרצו ללכת אליהם. אני הצבעתי 😇.



איך מצביעים:
  • נרשמים באתר: https://summit2017.reversim.com/
    • אני מקווה שישלחו למי שנרשם לאתר תזכורת להירשם לכנס. כל המקומות נתפסים בד"כ ביום הראשון להרשמה.
  • ניגשים להצעות: https://summit2017.reversim.com/proposals

יש כ 330 סשנים, ואין דרך כ"כ יעילה לפלטר אותם, ה tags הם בקשר של "and" - מה שהופך בחירה של כמה tags לדבר לא יעיל.

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

כשאתם מוצאים סשן שנראה מעניין - פתחו את הליק למטה ("Show more", אפשר גם פשוט להקליד על הכותרת) בטאב חדש - שלא תצטרכו להתחיל אח"כ מהתחלה.



אחרי שהצבעתם - אפשר לסגור את הטאב ולהמשיך לגלול.




ככה לפחות אני עבדתי.



שיהיה בהצלחה!



2017-07-21

קוטלין (Kotlin) למפתחי ג'אווה ותיקים - חלק ב': פונקציות

המשך לחלק א': קוטלין (Kotlin) למפתחי ג'אווה ותיקים - חלק א': הבסיס


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

כרגיל, ננסה ללמוד עוד קצת על הדרך:


  1. הנה כמה קל להשתמש בקוד ג'אווה: אני מבצע import מתוך java.lang.Math לפונקציית החזקה (power).
    בג'אווה, בכדי לייבא מתודה הייתי צריך להדגיש: import static. בקוטלין - ויתרו על התחביר המיותר.
    שווה לציין שכל שלכל קובץ של קוטלין מתבצע מאחורי הקלעים import לשורה של חבילות: *.java.lang וכמה חבילות של קוטלין.
  2. כאשר מספר הוא עשרוני, הקומפיילר אוטומטית מניח שהוא מסוג Double. לא צריך לכתוב d בסוף המספר (אבל כן צריך לכתוב F אם אתם רוצים שיוגדר Float).
  3. כך נראית פונקציה - שוב: מאוד דומה ל Go, מאוד "מודרני".
    שימו לב לטבעיות בשימוש בפונקציה pow שהגיעה מספריה בג'אווה.
  4. הדרך להפעיל (invoke) פונקציה - היא כמו בג'אווה.
  5. זו הדרך הפשוטה להציג רק 2 ספרות עשרוניות למספר - בעזרת פונקציית format של ג'אווה.
  6. אני יכול לקרוא לפונקציה תוך כדי שאני מציין את שמות הפרמטרים כמו שהוגדרו בפונקציה (ואז סדר הארגומנטים לא משנה).
    זה אולי קריא יותר, אבל ה IntelliJ מוסיף תגיות מאוד דומות בכל מקרה (כיביתי אותן כרגע).
  7. זו הדרך לבצע formatting לאלפים + 0 ספרות לאחר הנקודה - שוב, בעזרת פונקציית format של ג'אווה.

הנה הפלט:




Nullable Types

בוודאי אתם מכירים שה Exception הנפוץ ביותר בג'אווה הוא NullPointerExcpetion.

כמו שפות מודרניות אחרות, יוצרי קוטלין החליטו להגדיר את השפה כ "null-safe by default". כעיקרון, אין nulls, אלא אם אתם מתירים אותם.


  1. זו תהיה שגיאת קומפליציה. מה פתאום לקבוע ערך null? אין מצב!
  2. אם אנחנו מגדירים ש y הוא מסוג Nullable String - אז אפשר לקבוע בו ערך null. בסדר.
  3. גם זו שגיאת קומפילציה: השתגעתם? לקרוא למתודה ()length למשהו שעשוי להיות null? אנחנו לא רוצים nullpointerException!
  4. פה הקומפיילר חכם: אם כבר בדקתי שהערך איננו null - אז יאפשרו לי לקרוא למתודות על האובייקט ה nullable.
  5. דרך אחר לעשות זאת, להשתמש באופרטור ?. במקום . - שמגן עלי בפני nullPointerException.
    אם y הוא null - אזי תוצאות כל הביטוי תהיה גם היא null.
  6. מה קורה עם מחלקה שאני מביא מג'אווה? בג'אווה - כל האובייקטים הם Nullable.
    אתם רואים שאני צריך לבנות מבנה דיי מורכב (ולא מעניין) על מנת להגן על עצמי מ NullPointerException.
    החלטה תכנונית של שפת קוטלין היא לא להגן עלי בכוח בפני אובייקטים של ג'אווה - כי כך אני רגיל. ההגנה ברמת הקומפילציה היא רק בפני אובייקטים של קוטלין שהם Nullable.
  7. בכל זאת, קוטלין מספקת לי תחביר מקוצר ובטוח לעבודה עם אובייקטים שהם Nullable. התחביר הזה תופס גם לאובייקטים של ג'אווה, וגם לאובייקטים של קוטלין שהם Nullable (ואולי יש להם פונקציה שערך ההחזרה שלה הוא Nullable).
    במקרה הזה, מכיוון ש ()getExtID מחזיר null (נניח), כל הביטוי יוערך כ null - ולא ייזרק NullPointerException.
  8. לקוטלין יש כמה כלים לעבוד עם רשימות שחלק מהאיברים בהן עשוי להיות null.
    גישה ראשונה היא השימוש בפונקציה let שתפעל רק אם הערך הוא לא null.
    אפשר להשתמש ב let גם ללא קשר לרשימת איברים.
  9. גישה שניה, היא להשתמש בפונקציה / פילטר בשם ()filterNotNull, כלומר: להשאיר רשימה הכוללת רק איברים שאינם null.
  10. אני יכול לבדוק אם משתנה הוא null ולהגיב בהתאם.
  11. או בגלל שזו פעולה שכיחה, להשתמש באופרטור ה :? (נקרא Elvis Operator) - בכדי להשיג בדיוק אותו הדבר בכתיבה מקוצרת.
  12. אם אני ממש מתעקש, אני לומר לקומפיילר: "עזוב אותי באמאשלך! אני אסתדר עם ה null-ים שלי בעצמי", בעזרת אופרטור ה .!!
    1. שימו לב שאת האופרטור הזה אפשר להפעיל רק על משתנים שהם immutable (למשל: val) - אחרת, יכול תאורטית thread אחר להיכנס בין "שורות" ההפעלה שלי - ולשים בו null.
      1. אופס!, חטפתי KotlinNullPointerException.


הנה הפלט:




ערכי החזרה מיוחדים


שימו לב לחבר'ה הבאים:



1. לפונקציה foo לא הגדרנו ערך החזרה. היא בעצם בעלת ערך החזרה מטיפוס Unit (בצורה לא מפורשת).


    Unit הוא כמו void בג'אווה - אבל ניתן לשמור את הערך (עבור גנריות של הקוד).
    אם יופיע בלוג "kotlin.Unit" - מכאן זה מגיע.

2. ניתן להגדיר את אותה הפונקציה כ Unit בצורה מפורשת.
3. הפונקציה fail היא מסוג Unit, אבל בעצם היא לעולם לא תחזיר Unit - כי תמיד היא זורקת Exception (שימו לב שבקוטלין לא משתמשים ב new).
4. ניתן להגדיר פונקציה שלעולם לא תגיע ל return - כבעלת ערך החזרה מטיפוס Nothing.


את Nothing לא ניתן להציב במשתנה - זו בעצם הערה לקומפיילר.

5. כאן נקבל שגיאת קומפליציה מסוג Conflicting declaration - הקומפיילר לא יודע איזה טיפוס להגדיר ל data:
    Int - בגלל Zoo?
    או אולי Any (ה root בהיררכיית האובייקטים בקוטלין, כולל null) - בגלל ש fail הוא מטיפוס Unit?

6. במקרה הזה הקופיילר רגוע: מ ()gail לא יכול לחזור ערך (הוא מסוג Nothing), ולכן data יוגדר כטיפוס Int.


מעניין אולי לציין ש null הוא singleton מטיפוס ?Nothing.





default value ו vararg



  1. אני יכול להגדיר בפונקציה פרמטרים עם ערך ברירת מחדל - למשל: cores.
  2. אני יכול לקרוא לפונקציה עם כל הארגומנטים (ה annotations הם של ה IDE).
  3. אני יכול לקרוא לפונקציה ולהשמיט את הארגומנט - ואז לקבל את ערך ברירת המחדל.
  4. הנה הגדרתי פונקציה לחישוב ממוצע ה CPU utilization במכונה - ע"י חישוב הממוצע של ה cores.
    שימו לב שניתן להשתמש בכתיב מקוצר (=), אם הפונקציה גוף הפונקציה הוא ביטוי (expression) בודד - גם אם אין ערך החזרה. הגדרה כזו נקראת shorthand function.
  5. אני יכול גם להגדיר מספר משתנה של פרמטרים - בעזרת המילה vararg (המקבילה של ... בג'אווה).
    המשתנה core הוא מטיפוס <Array<out T  - אסביר את הנושא של out, כשנגיע ל Generics.

    אם יקראו ל printStats עם 2 פרמטרים - הפונקציה הראשונה תופעל.
    אם ל printStats עם מספר שונה של פרמטרים - הפונקציה השנייה תופעל.


התוצאה:




פונקציות מסדר גבוה

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


  1. הנה אנחנו מגדירים פונקציה בשם profile המקבלת פונקציה אחרת (func) כפרמטר.
    התחביר הוא סוגריים עם רשימת משתנים, חץ (<-) וערך החזרה.
    מעבר לכך, זהו פרמטר לכל דבר.
  2. בכדי להוסיף ערך מוסף כלשהו, השתמשתי בפונקציית ה profiling של ג'אווה, המחזירה את הזמן הנוכחי בננו-שניות.
  3. הפעלה של הפונקציה func נעשית בצורה טבעית למדי.
  4. עכשיו אגדיר 2 פונקציות דומות המתאימות בחתימה לפרמטר func של profile: מקבלות 2 מספרים שלמים - ומחזירות ערך של מספר שלם
  5. כעת אני סקרן לדעת איזו פונקציה יעילה יותר מבחינת ביצועים!
    כך אני מפעיל את הפונקציה "הגבוהה" profile:
    הקומפיילר צריך לדעת שאני רוצה לשלוח רפרנס לפונקציה ולא להפעיל אותה, את זה עושים בעזרת האופרטור :: - המחזיר reference לפנוקציה.
    הקפדתי על ארגומטים עם ערך זהה, על מנת לא להטות את בדיקת הביצועים 😉
    אם אני רוצה רפרנס למתודה של אובייקט, התחביר הוא object::method.
  6. מה קורה כאשר אני רוצה להגדיר חתימה של פונקציה שלא מחזירה כלום?
    אם אגדיר רק (Int) - הקומפיילר יתעלם מהסוגריים ויניח שמדובר בפרמטר מסוג Int.
    עלי לציין ערך החזרה מסוג Unit (כפי שראינו מוקדם יותר בפוסט) - ואז הקוד יתנהג כמצופה.

הנה הפלט:


אלף ננו-שניות הן מיקרו-שניה (ולא מילי-שניה, לא לבלבל!).
הממ... משהו מוזר לי בזמנים של הפונקציה add - הייתי מצפה לכ 50,000 ננו-שניות (5 הפעלות של פונקציה) ומטה (אופטימיזציות של הקומפיילר)...


Lambda Expressions

בואו נסתכל שוב על הפונקציות add ו addFast שלמעלה. עבור שימוש חד פעמי - הייתי צריך להגדיר פונקציה, מה שלוקח עוד קצת מקום אבל בעיקר - מנתק את הקוד מההקשר.

לא היה נחמד יותר לכתוב את גוף הפונקציה inline?



  1. הנה אני יכול לכתוב את הפונקציה inline, כ anonymous function. זה התחביר - ללא שם. זה עדיין לא מאוד אלגנטי.
  2. יותר אלגנטי יהיה לכתוב את אותה הפונקציה inline כ lambda expression. זה התחביר.
  3. מכיוון שהקומפיילר יודע מה החתימה של profile - זה מיותר לציין לו טיפוסים על הפרמטרים, ולכן ניתן לכתוב את הפונקציה בצורה קצרה יותר.
  4. חשוב לציין שאם אני עושה extract variable ל lambda expression, והיא כבר לא inline - הקומפיילר לא יידע להסיק את הטיפוסים של הפרמטרים - ויש לציין אותם בחזרה.
  5. לצורך ההסבר, נעבור לעבוד עכשיו עם פונקציה דומה ל profile בשם profileOne, המצפה לפרמטר אחד בלבד.
    שימו לב שהחלפתי את סדר הפרמטרים בפונקציה, כאשר func הוא הפרמטר האחרון. מייד אסביר מדוע זאת היא הקונבנציה ב higher order functions בקוטלין.
  6. כאשר יש פרמטר יחיד לפונקציה, לא צריך להגדיר אותו. אפשר להשתמש בצורה מקוצרת המדלגת על ההגדרה (<- num) ישר לגוף הפונקציה. אם לא הוגדר שם משתנה - ההתייחסות אליו תהיה כ it.
    זה יכול להיות קיצור נחמד, אבל הייתי ממליץ לכם להימנע ממנו כאשר יש פונקציות מקוננות - אז זה נהיה מבלבל.
  7. הנה הסיבה מדוע הקונבנציה בקוטלין היא שהפונקציה תהיה הפרמטר האחרון: אם ורק אם הפונקציה היא הפרמטר האחרון, אפשר לכתוב את גוף הפונקציה מחוץ לסוגריים.
  8. למה זה טוב? זה שימושי אם אתם רוצים לייצר משהו שדומה ל DSL (יש עוד מה להרחיב בנושא בפוסטי המשך).
    למשל: הגדרתי את הפונקציה enhance (שאולי מוסיפה לוגים, בדיקות אבטחה, ניטור, וכיוצ"ב) ואז אני פשוט שולח לה קוד. זה דומה מאוד ל DSL.
  9. אשנה קצת את ה layout של הקוד בכדי להדגיש: אתם יכולים להגדיר פונקציות שלכם, שנראות "כמו" מילה בשפה. כמה מבלבל הוא לראות enhance שכזה (מבלי להכיר את קוטלין) - ולא למצוא בגוגל מה הוא עושה 😏

אז עם כל התחביר הנחמד הזה של למבדה, מה הטעם להשאיר את ה Anonymous Function?
יש מקרים שבהם הוא בכל זאת שימושי, למשל: כאשר אתם רוצים לעשות return מתוך מקומות שונים בתוך הפונקציה.
מקרה אחר: אני יכול להגדיר return type - שאולי הוא subtype של מה שמגדירה הפונקציה מסדר-גבוה, המארחת.



Closures

פונקציה אנונימית ו lambda expression מסוגלים לגשת למשתנים של הפונקציה שעוטפת אותם - וגם לשנות אותם (זה שוני מג'אווה).


הנה דוגמה פשוטה עבור ההסבר.
למשתנה n, שנמצא באותו ה closure עם הלמבדה - אני יכול לגשת.
למשתנה m, שנמצא בפונקציה עוטפת, אך לא בתוך ה closure - אני לא יכול לגשת.

משתנה שניגשנו אליו בתוך Closure לא "יוקפא הערך שלו" - כמו בג'אווהסקיפט, למשל.

לאחר הסרת השורה הבעייתית, הנה הפלט:





סיכום


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

האם סיימנו לכסות את נושא הפונקציות?
עוד לא.

יש עוד Local Function, Infix Functions, Inline Functions, Extension Functions - ואולי עוד שכחתי משהו.

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

הנה פוסט ההמשך.


שיהיה בהצלחה!