יום רביעי, 18 באוקטובר 2017

Evolutionary Design - מצגת מרברסים 2017

היי,

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

תודה לכל מי שבא, ותודה על הפירגונים - זה ממש שימח אותי!





ליאור

יום שבת, 16 בספטמבר 2017

קוטלין (Kotlin) למפתחי ג'אווה ותיקים - חלק ז': הספריה הסטנדרטית, וכתיבת קוד אלגנטי

פוסט זה הוא המשך של:
קוטלין (Kotlin) למפתחי ג'אווה ותיקים - חלק א': הבסיס
קוטלין (Kotlin) למפתחי ג'אווה ותיקים - חלק ב': פונקציות
קוטלין (Kotlin) למפתחי ג'אווה ותיקים - חלק ג': מחלקות
קוטלין (Kotlin) למפתחי ג'אווה ותיקים - חלק ד': עוד על מחלקות, אובייקטים, ועוד...
קוטלין (Kotlin) למפתחי ג'אווה ותיקים - חלק ה': DSLs
קוטלין (Kotlin) למפתחי ג'אווה ותיקים - חלק ו': Collections ו Generics


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



Scope Functions


נתחיל בפונקציות ה"חדשות" ביותר למפתחי ג'אווה - או כך לפחות נדמה לי: ה scope functions.
הדמיון הרב ביניהן - הוא דיי מבלבל!

נפתח בהגדרות:

לנסות לשנן את הטבלה הזו - זה אחד הדברים המטופשים שניתן לעשות! היא נועדה ל reference.

את הפונקציה ()with, אני מניח שכולם מכירים. אני זוכר אותה עוד מימי Object Pascal...

הפונקציה ה"כמעט-תאומה" שלה היא apply:

  1. כפי שאתם רואים הן דיי דומות: משתמשים בהן כאשר רוצים לבצע שורת פעולות על ביטוי מורכב (או סתם משתנה עם שם ארוך), כאשר:
    1. ב with שולחים את הביטוי כפרמטר.
    2. ב apply - כ extension function על הביטוי.
  2. יש גם הבדל בערכי ההחזרה:
    1. הביטוי של with יחזיר את ערך (כלומר: ה evaluation) של הבלוק.
    2. הביטוי של apply יחזיר את האובייקט עליו הופעלה apply.
  3. זה כל ההבדל? בשביל זה יצרו שתי פונקציות כ"כ דומות?
    האם apply היא פשוט עבור עצלנים שלא מסוגלים לעשות extract variable?!
  4. ובכן... דווקא ערך ההחזרה הוא החשוב - המאפשר ב apply לשרשר את הפעולה. זה מתאים לשרשרת פעולות שכבר אין לכם "ביד" את ה reference לאובייקט המדובר - ואז apply מאפשרת את המשך השרשור.


הפונקציה הבאה שנפגוש, ()run - עשויה להישמע קצת מוזרה: היא רק מריצה בלוק.
את הבלוק שתתנו לה - היא תריץ.

מה הטעם בכזו פונקציה? למה היא שימושית?!

טוב... הדוגמה הראשונה באמת מעוררת השתוממות.

הדוגמה השניה - מסבירה את העניין:
כאשר אתם מריצים את run - אתם יוצרים scope חדש/נוסף להרצה.

אם אתם רוצים להימנע מלכלוך ה scope שלכם, למשל במשתנה temp - הפונקציה run תאפשר לכם לעשות זאת בצורה אלגנטית. שימוש ב run מצהיר בצורה מפורשת: "temp קיים רק עבור הפעולה הקצרה הבאה - ואינו רלוונטי להמשך הקוד"

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


קיימת גם פונקציית run שרצה כ extension function, הדומה קצת apply:


run המקבלת למבדה מתאימה, כמו apply, לפעולות שרשור - אבל ערך ההחזרה שלה הוא ה evaluation של הבלוק.
היא שימושית כאשר יש שרשור, ואנו רוצים לבצע חישובים על האובייקט ואז להחזיר ערך - למשל: פונקציית ה ()genrate בדוגמה שסיפקתי.
כמו apply - היא "חיה" ב scope של האובייקט (כי היא extension function), ולכן קיימת גישה לפרמטרים של האובייקט.

אפשר לראות אותה בדוגמה הקאנונית מלמעלה - דיי דומה ל apply.
בכל אופן, הדוגמה הראשונה (Password generator) היא הדוגמה המציאותית יותר - ובמיוחד אם מדובר בשרשור.


שאלה: האם ()x.applyAndReturn היה יכול להיות שם מוצלח יותר ל ()x.run?



הגענו לזוג האחרון: let ו also.

הפונקציה let דומה לפונקציה map, כאשר היא פועלת על איבר יחיד.


במקום להיות extension function, היא מעבירה את האובייקט עליו היא פועלת - כפרמטר (it).
היתרון שבכך?

במידה ואתם רוצים בבלוק להתייחס ל this - האובייקט החיצוני. פונקציות כמו apply עושות shadowing ל this המצביע לאובייקט בו רצים. let לא עושה זאת.

כמו כן, let מחזירה את ה evaluation של הבלוק.

שימוש נפוץ ב let הוא כתיבה קצרה להגנה בפני null:

  1. הדוגמה הזו נכשלת בקומפילציה: מכיוון שמדובר ב property ולא משתנה "אטומי", ייתכן ומאז בדיקת ה null ועד להפעלת הבלוק - ייכנס ל property ערך אחר null-י ש"יפיל" אותנו.
  2. דרך אחת בטוחה היא להעתיק עותק מקומי למשתנה - ולבדוק אותו. הכי טוב val.
  3. דרך יותר קצרה ואלגנטית, היא השימוש ב let: הפונקציה מוערכת ברגע אחד מסוים - כשה evaluation של הביטוי עליה פעלה כבר בזיכרון:
    1. אם ה evaluation הוא null - כל הבלוק לא ירוץ.
    2. אם ה evaluation אינו null - הבלוק ירוץ, וניתן להתייחס ל it בבטחה כ not-null.

שם אפשרי אחר לפונקציה ()let היה יכול להיות ()ApplyItAndReturn.


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

היתרון: היכולת לשרשר.

אני אשאיר לדימיון שלכם לתת לה שם יותר משמעותי....


בקרוב תצא קוטלין 1.2 עם פונקציות ה scope החדשות: ()due(), just  ו ()bound.  

סתתתאאאם! 😉








Streams


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

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

בהשוואה בזכוכית מגדלת, כל מימוש "פונקציונלי" היה קצר פי כמה - מהמימוש המקביל בג'אווה.

בג'אווה השתפרו עם הזמן, ובג'אווה 8 הציגו את יכולות ה Stream - יכולות פונקציונליות בשפת ג'אווה ועל גבי ה Collections הסטנדרטיים שלה... עם כמה wrapper שנדרשים.

אי אפשר היה להתעלם מצהלת השמחה בקהילת הג'אווה, שחשה גאווה רבה:


אמנם צריך להוסיף את המילה המעצבנת stream, וגם Collector לפעמים - אבל זה היה בהחלט נסלח, מול היתרונות.


האמת?

קוטלין הוציאה גם פה את ג'אווה באור לא מחמיא. היא מצליחה לפשט משמעותית תחביר ה stream של ג'אווה לתחביר נקי הרבה יותר.

למשל, הדוגמה הלא-מחמיאה הבאה:


יכולה להיכתב בקוטלין כך:


השילוב של ה Collections של קוטלין, ויכולות השפה (פרימיטיביים הם אובייקטים, extension functions, ועוד) - הופכות את היתרון -> למשמעותי מאוד.

הערה של קורא: נכון: הדוגמה של קוטלין עושה רק print ולא println ולכן היא קצרה יותר. טעות שלי.



חזרה על עקרונות הבסיס של Streams

ביטוי סטרימי (Stream-י) יהיה בנוי מ:
  • מקור: מקור נתונים. בדר"כ מבנה נתונים מאולס של Java שהופעלה עליו הפונקציה ()stream או Stream שנוצר במיוחד.
    • ה Stream עשוי להיות "אינסופי" (למשל: רצף מספרים אקראיים) ולכן ניתן להגביל את מספר האלמנטים בהם רוצים לטפל בעזרת הפונקציה ()limit.
    • רצפים אינסופיים ניתן לייצר בעזרת פונקציית (Stream.generate(lambda - כאשר lambda מספקת את הערך הבא, או (Stream.iterate(lambda - כאשר lambda מספקת את הערך הבא, תוך כדי שהיא מקבלת את התוצאה הקודמת כפרמטר.
  • פעולות ביניים (Intermediate Operations)
    • אלו פעולות שמקבלות Stream ומחזירות Stream - כך שניתן לשרשר אותן, ולהרכיב אותן זו על זו - בכל הרכב שנבחר. למשל: (...)filter(...), map, או (...)limit
    • באופן מעשי, הפעולות לא מחויבות לפעול ברגע (או סדר) ה evaluation שלהן - כך שמתכנני מגנון ה Streams יכולים להוסיף אופטימיזציות שונות. 
    • מה שיגרום לשרשרת הפעולות להתחיל ולפעול - הוא המצאות פעולת הסיום.
  • פעולת סיום (Terminal Operation) היא התוצאה המצופה מן כלל ביטוי ה Stream.
    • זוהי פונקציה שמקבלת Stream אבל לא מחזירה Stream (בהכרח). למשל: ()sum(), findFirst, או ()findAny.
      • השם findFirst הוא קצת מבלבל: למה צריך "לחפש" את האיבר הראשון?
        • בפועל: לא מחפשים אותו (זמן הריצה יהיה (O(1) - אבל זהו אילוץ שמחייב את ה Stream לשמור על סדר האיברים.
        • כאשר מפעילים את ()findAny - אין אילוץ כזה. בד"כ יחזור האיבר הראשון, אבל לפעמים יחזור איבר אחר מהרשימה (אם הופעלה איזו אופטימיזציה).
    • פעולות סיום נפוצות אחרות הן:
      • (forEach(lambda - שיכולה לבצע פעולה שרירותית כמו הדפסה של האיברים, אבל אחד אחרי השני ולפי הסדר.
      • (reduce(lambda - שיכולה לבצע "סיכום של תשובה" כאשר מגיעים אליה 2 פרמטרים: תשובה חלקית, והאיבר הבא (נניח: חישוב ממוצע מסוג מסוים). בשימוש בה - ניתן לבצע אופטימיזציות על ה Stream.

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

קוטלין מימשה מנגנון Steam משלה, שנראה מאוד דומה - אך בנוי באופן מובנה על ה collections של השפה.
המימוש ה default-י של streams בקוטלין אינו כולל lazy evaluation ואופטימיזציות או יכולות מקבול כמו בג'אווה. אפרט עוד על ההבדלים בהמשך.
 את תחביר ה Streams בקוטלין מפעילים ללא פעולת ה ()stream - בכדי להתחיל stream, ולא צריך את פעולות ה ()collect על מנת להמיר אותו חזרה ל collection ולטפל בטיפוסים שונים:

בקוטלין אפשר פשוט לסיים את פעולת ה Stream ב ()toList בכדי לקבל רשימה.
רוצים מערך? השתמשו ב: ()toList().toTypedArray.

מפה? השתמשבו ב ()associate  או ()associateBy:


גם נושא האינדקסים סודר בקוטלין, ויש פונקציות (למשל: ()mapIndexed) המאפשרות לגשת לאינדקס האיברים ב stream.

האמת: עברתי על 10 השאלות הנפוצות של התג "java-stream" באתר stackoverflow כדי לראות במה כדאי לעסוק בפוסט - ובקוטלין כיסו בצורה אלגנטית את כל הבעיות שהופיעו ב 10 השאלות הללו. נראה לי שגם הם הסתכלו - על אותה הרשימה בדיוק.


הפעולות בקוטלין בעלות שמות זהים ברוב המקרים. הנה כמה הבדלים:
  • findFirst ו findAny נקראות first ו any - בהתאמה.
  • limit נקראת בקוטלין take.
  • peek (כמו forEach, רק שמחזירה Stream) נקראת בקוטלין onEach (שם יותר ברור- לטעמי).


מעבר לכך - הטיפול ב Streams הוא ממש דומה.

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

  • flatmap היא שימושית כמובן כאשר אנו מפעילים פונקציה שמייצרת רשימה - אף אנחנו רוצים את האיברים שבה, או כאשר אנחנו רוצים להפוך איבר אחד ב stream - למספר איברים.
  • חשוב לזכור ש filter משאיר (ולא מסיר) - את מי שעומד בתנאי.
    • filterNot - מסיר.

  • ()takeLast הוא ההופכי ל take, ו ()drop - הוא המשלים.
  • ()takeWhile ימשיך לקחת איברים כל עוד הפרדיקט נכון. ברגע שנתקל בתנאי שלילי - הוא יעצור.

הנה כמה פעולות סיום נפוצות:

  • כמה פעולות סיום, כמו ()last ו ()first מופיעות ב 2 צורות: כפונקציה ללא פרמטרים, או כפילטר עם הפעולה מובנה. 
    • הצורה האידיומטית היא צורת הפילטר - כאשר זה אפשרי.
  • ()single תזרוק Exception אם לא נמצאו איברים, או שנמצא יותר מאיבר אחד. 
    • יש גם גרסאת ()singleOrNull - שפשוט מחזירה null.
  • ()fold היא ()כמו reduce, רק שהיא מקבל כפרמטר ערך התחלתי לעבוד עליו. במקרה שלנו - אפס.
    • יש גם ()foldRight שפשוט תפעיל את הפעולה בסדר הפוך: מהאיבר האחרון - לראשון. במקרה של חיבור התוצאה תהיה זהה.


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

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



Late Evaluation


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

לצורך כך בקוטלין יש מנגנון דומה לזה של ג'אווה של lazy evaluation הנקרא Sequences (שם שונה על מנת למנוע התנגשות בשם מחלקות).


בקוד הקוטלין, כל מה שצריך להוסיף הוא ()asSequence בתחילת הביטוי.
הפונקציה asSequence  ממירה את ה collection ל lazily evaluated sequence, בדומה ל Steam של ג'אווה.

לאובייקט ה Sequence יש מימושים מתאימים ל filter, map, first ועוד - כל הפונקציות שיכולות לאפשר מצב של אופטימיזציה.

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

המחיר של השימוש ב sequence הוא שלא יהיו לנו זמינות סט הפעולות שלא יכולות לעבוד במוד של lazy eval כמו ()takeLast או ()foldRight. במקרים מעטים, בהם יש עבודה אינטנסיבית שנהנית מ memory / resource locality - ה Sequence עלול להיות פחות יעיל.


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

במידה ואתם כותבים תשתית חישובית ל big data, רוצים parallel streams - עליכם להשתמש בתשתית ה Streams של ג'אווה (עדיין אפשר לכתוב את הקוד בקוטלין).



סיכום



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


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



יום שישי, 15 בספטמבר 2017

רברסים 2017 - הרצאה על Software Design


ההרשמה לרברסים 2017 נפתחה!

לעניות דעתי - זה הכנס הטוב ביותר בהייטק הישראלי.


אני מרצה על Software Design (ראשי התיבות הם סתם המצאה - אין ראשי תיבות כאלו מקובלים), ובמקביל לחיים ידיד - שעובד איתי!


אם אתם מגיעים לכנס - קפצו להגיד שלום!


ליאור

יום שני, 4 בספטמבר 2017

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

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


הפעם נדבר על Collections ו Generics - נושאים שעברו כמה התאמות מהגרסה הג'אווה-ית.


Generics - תזכורת


מהם בעצם Generics ("חסרי ייחוד")?
הותיקים-באמת שביננו זוכרים את הימים של Java 1.4 בה כל collection בשפה היה מטיפוס Object. בכל שליפה של איבר מתוך הרשימה - היה צריך לבצע פעולת Down-Casting.


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

בשל שיקולים של תאימות-לאחור, ה generics בג'אווה (וליתר דיוק: ב JVM) הם ברמת הקומפילציה (ולא ה runtime). יש שלב של הקומפיילר בשם Type Erasure בו הוא מוחק את ה generics, ומחליף אותם במבני-נתונים מסוג Object עם down-castings מתאימים + מוסיף בדיקות שפרמטרים שהוזנו למתודות הם מהטיפוס הנכון.

זהו. זה כל מה שמפתח צריך לדעת על Generics, לא?!


יש קצת יותר.
אפשר להשתמש ב generics במחלקות שלנו, ולא רק ב Collection המסופקים ע"י ג'אווה.

למשל, אני רוצה לממש Repository כללי בנוסח DDD - אבל אם אשתמש ב Any בתור טיפוס, לא אוכל להתייחס לתכונות הספציפיות של האובייקטים שבהם אני משתמש.

  1. מי ששולף Entities מה Repository צריך לעשות downcasting - בקוטלין, בעזרת המילה השמורה as.
  2. אין בדיקה ברמת הקומפילציה שאני שולח ערכים רלוונטיים לפונקציות... אאוץ.
  3. אני לא יכול בתוך המחלקה Repository להתייחס לתכונות הספציפיות של האובייקט שאני רוצה להשתמש בו.

כאשר אני משתמש ב Generics - הדברים נראים אחרת:

  1. אני מגדיר ב scope של ה class טיפוס בשם T, ממש לפני הגדרת המחלקה שבסוגריים המסולסלים. 
    1. אני יכול להשתמש עכשיו ב T במקום טיפוס, בכל מקום בקוד של המחלקה.
    2. ברגע שייווצר instance של המחלקה הזו, טיפוס מסוים היה קשור אליה, וכל התייחסות ל T - בעצם "תוחלף" ע"י הקומפיילר בטיפוס שהוגדר.
  2. אין צורך להצהיר על downcasting מתודות שליפה. הקומפיילר דואג לכך.
  3. הקופיילר יאכוף שהערכים שנשלחים הם מהטיפוס הנכון.
  4. עדיין אני לא יכול לגשת בתוך המחלקה לתכונות של הטיפוס הספציפי.
  5. זה נפתר ע"י כך שאגדיר את הטיפוס: T היורש מ Entity.
    1. הקומפיילר יוודא שטיפוסים שנקשרים למחלקה יורשים מ Entity.
    2. כך בתוך קוד המחלקה, אוכל להניח של T יש את כל התכונות / פונקציות הזמינות של Entity.
  6. שימו לב ש T כברירת מחדל הוא מטיפוס ?Any. אם ארצה שהטיפוס יהיה לא-nullable יהיה עלי להגדיר: <T : Any>

אפשר להגביל את הטיפוס הגנרי ("T") אף יותר, ולחייב שירש / יממש יותר ממחלקה - כלומר: גם ממשקים. את האכיפה הזו עושים בתחביר המשתמש במילה where: 

  1. רק טיפוס שגם יורש מ Entity וגם מממש את הממשק Comparable - יוכל להיקשר למופע של המחלקה. 
  2. where הוא המקביל של קוטלין לצורה <T extends ClassA & InterfaceB> של ג'אווה.

מדוע משתמשים ב "T" לתאר את הטיפוס הלא ידוע? מתי יש שמות אחרים?
הקונבנציה אומרת ש:
  • T - אם יש משתנה אחד.
  • S - אם יש משתנה שני, U - אם יש משתנה שלישי, ו V - אם יש משתנה רביעי.
    אפשר לזכור את הסדר כ "SUV" - השם האמריקאי ל"ג'יפון עירוני".
  • K ו V - אם יש צמד key  ו value, למשל ב Map.
  • E - כדי לתאר אלמנט במבנה נתונים.
  • N - לתאר טיפוס שהוא מספר.
  • R - לתאר טיפוס החזרה (return value).



"חורים" ב Generics


בג'אווה קיימת הבעיה הבאה:


אני יכול להגדיר מבנה נתונים מסוג <List<String, בכדי לקבל הגנה של הקומפיילר.

אבל... אם המתודה שלי, במקרה הזה ()unsfaeAdd (שעשויה להימצא במקום אחר ומרוחק בקוד), מצהירה על ממשק כללי List (להלן "raw type") - הקומפיילר יאשר את הקוד: הרי <List<String הוא List - חייבים זאת עבור תמיכה לאחור.

הממשק List מתאר מתודה (add(Object o המקבלת אובייקט מכל סוג - מה שיאפשר לי להכניס גם אובייקטים מסוג אחר לרשימה. הכישלון בזמן ריצה יהיה רק ברגע השליפה, כאשר מנסים לעשות casting (שהוסיף הקומפיילר לקוד):


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



Variance


הפתרון המקורי של ג'אווה היה להוסיף כלי שנקרא wildcard (מסומן כ ?).


במתודה שלי (נניח שהיא במקום מרוחק בקוד) אני מצהיר שאני מקבל רשימה - אבל לא יודע באמת מה הטיפוסים שמאוחסנים בה (<?>List נקרא כ "List of some type"). הקומפיילר לכן יאפשר לי לבצע רק פעולות שליפה - אך לא פעולות השמה. זוהי בעצם הגנה בפני התנהגות לא צפויה.

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

בכלל, כמה אנשים נוחים עם ההגדרה הבאה?


בסה"כ מדובר בספריה הסטנדרטית של ג'אווה: (...)Collections.min. נרצה בוודאי להבין מה שכתוב בתיעוד.

אנחנו מכירים כבר את &, ועד סוף הפוסט, הביטוי (המורכב) הזה - יהיה ברור לחלוטין.


אם נדבר בשפה פורמאלית, אזי String הוא variance של Object - כי הוא יורש ממנו, אבל <List<String הוא invariant של <List<Object - כי הוא לא יורש ממנו (הוא יורש מ <Collection<String).


הפתרון של קוטלין לבעיה הנ"ל הוא מעט אחר, ומוממש כחלק מהספריה הסטנדרטית.
הספריה הסטנדרטית (stdlib) של קוטלין היא קטנה בהגדרה (כ 750KB) - ומכילה התאמות לג'אווה.

בגזרת ה collections, קוטלין לא מציגה collections חדשים מאלו של ג'אווה (Set, Map, Array, List) - אלא רק עוטפת ומרחיבה אותם (פעמים רבות - בעזרת extension functions).

קוטלין מספקת ממשקים למבני-נתונים (Map, List) מ-2 סוגים:
  • Immutable Interfaces - שהם ברירת המחדל, כאלו שניתן רק לשלוף מהם.
    • למשל:<List<E ו <Map<E
  • Mutable Interfaces - כאלו שניתן לבצע בהם גם שינויים.
    • למשל: <MutableList<E  ו <MutableMap<E


התחליף של קוטלין, אם כן, ל wildcard של ג'אווה הם immutable interfaces. 
בהגדרת הפונקציה unsafeAdd קוטלין לא מרשה לי להשתמש ב Raw type כמו List - אלא רק במבנים עם הגדרה גנרית.



הנה אנסה כמה תצורות נוספות:

  1. כאן יש שגיאת קומפילציה: ניסיתי להוסיף איבר למבנה נתונים שהוא immutable - אסור. זוהי ההגנה המקבילה ל wildcard.
  2. כאן הגדרתי שאני רוצה מבנה נתונים שניתן לבצע בו שינויים. אבל מה? מכיוון שהגדרתי את list מטיפוס String - הקומפיילר לא מוכל לקבל any.
  3. הנה התיקון - ביצעתי המרה מסודרת של o למחרוזת - והכל תקין.

הפתרון של קוטלין, להגדיר immutable collections הוא פשוט יותר מהפתרון של ג'אווה, הוא לכאורה "לא מפורש".
הסמנטיקה של immutable collections שימושיים למדי גם ל "functional-like programming" ול concurrency.

corner case שכן הפסדנו בקוטלין, הוא היכולת לעשות ()clear או ()remove ל collection המכיל איברים מסוג לא ידוע. אין סכנה להסיר איברים מסוג "לא ידוע", ולכן ניתן לעשות זאת ב <?>List, אבל לא ניתן לעשות זאת ב immutable list.

Tradeoff הגיוני, לדעתי.



אוקיי. פתרנו מקרה אחד בעייתי של Generics, אבל יש עוד מקרה בעייתי:

נ.ב. - קוד דומה גם לא יתקמפל בג'אווה
הרי: Int הוא מספר (יורש מ Number) - ולכן אני מצפה שהקוד תעבוד.
הבעיה: <Array<Int אינו יורש מ <Array<Number - הם invariants.

Immutable collection לא יעזור כאן. מה עושים?



Covariance & Contravariance


נפתח בהגדרה.

מבנה גנרי כלשהו Something המקיים ש:
  • טיפוס T הוא  subtype  של טיפוס A
  • וגם ניתן להתייחס ל <Something<T כ  subtype  של <Something<A
נקרא covariance.

בג'אווה אפשר להגדיר קשר של covariance בצורה הבאה:

Something<? Extends A>

אנו משתמשים ב ? ולא T - כי אנחנו לא רוצים להתייחס לטיפוס ספציפי בקוד: כל פעם יכול בעצם להופיע טיפוס אחר, שמרחיב את A.

הנה דוגמה:


מה ניתן לעשות במתודה ()foo?

קריאה
  • אפשר להתייחס לכל איבר בשלושת הרשימות כ Number - כולם כאלה.
  • אי אפשר להתייחס לכל איבר בהכרח כ Integer - כי אז "אפול" בטיפול ב list3.
  • אי אפשר להתייחס לכל איבר בהכרח כ Double - כי אז "אפול" בטיפול ב list2.
כתיבה
  • לא ניתן להוסיף לרשימה Integer - כי אז "אפול" ב list3.
  • לא ניתן להוסיף לרשימה Double - כי אז "אפול" ב list2.
  • לא ניתן להוסיף לרשימה גם Number - כי אז "אפול" ב list2 וב list3 המחייבות טיפוסים ספציפיים (אחרת ניפול בשליפה + casting, כמו בדוגמה למעלה).



Generics. הקומפיילר יעזור למנוע טעויות.




Contravariance

הקשר בו מבנה גנרי כלשהו Something מקיים ש:
  • טיפוס T הוא  supertype  של טיפוס A
  • וגם ניתן להתייחס ל <Something<T כ  supertype  של <Something<A
נקרא contravariance.

בג'אווה אפשר להגדיר קשר של covariance בצורה הבאה:

Something<? Super A>

בואו נשתמש בדוגמה:


מה ניתן לעשות במתודה ()goo?

קריאה
  • אי אפשר להתייחס לכל איבר בהכרח כ Integer - כי list2 ו list3 לא מכילים Integers בהכרח.
  • אי אפשר להתייחס לכל איבר בהכרח כ Number- כי list3 מכיל אובייקטים שונים.
  • ניתן רק להתייחס לאיברים כ Object - כי תמיד הם יהיו כאלה.
כתיבה
  • ניתן, מן הסתם, להוסיף לרשימה Integers - כי כל הרשימות יכולות להכיל Integers - בהגדרה.
  • ניתן להוסיף subtypes של Integer לו היו: למשל, אם היה PositiveInteger שהיה subtype של Integer.
  • לא ניתן להוסיף Double או Number, וגם לא Object - כי תהיה לנו את list1 שבה מתבצעת בדיקה שנכנסים רק Integers (או subtypes), כדי להימנע מהבעיה של שליפה + casting שראינו למעלה.


Generics. הקומפיילר יעזור למנוע טעויות [א].



ובחזרה לקוטלין...


הסמנטיקות של ג'אווה,  extends A ? ו super B ? הן מוצלחות בלהזכיר מתי ? יורש מ A, או מתי הוא אב של B - אבל לא כ"כ מוצלחות בלהזכיר לנו את ההתנהגות הצפויה: מה מותר לקרוא ומה מותר לכתוב. זה לא self-explanatory.

בכדי לעזור לזכור, ג'ושוע בלוך הציג את הכלל הבא: "Producer Extends, Consumer Super", או בקיצור PECS.

הווה אומר:
  • אם המבנה הגנרי מספק ערכים (Producer / אנו קוראים ממנו) - השתמשו ב extends, ויהיה לנו אסור להוסיף פריטים לרשימה.
  • אם המבנה הגנרי צורך ערכים (Consumer / אנו כותבים אליו) - השתמשו ב super, אך לא נוכל להסתמך בקריאה על איזה טיפוס יצא.

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

  • Something<out T>   ==> producer
  • Something<in T>   ==> consumer

במקום לחשוב איזה טיפוס לא ידוע ירחיב או יירש מ T - אנו פשוט מצהירים:

  • האם אנחנו מתכוונים לשלוף ערכי T (או בנים שלהם) - בשימוש ב out.
  • או האם אנחנו הולכים להכניס למבנה ערכי T (או אבות שלהם) - בשימוש ב in.

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

כמובן שאנחנו יכולים גם להסתפק בתחביר הפשוט <Something<T שאומר - שליפה והכנסה יעשו בדיוק עם הטיפוס T. ברוב המקרים של שימוש ב generics אין באמת צורך להשתמש ב variants.

בואו נראה את in ו out בשימוש. הנה למשל ההגדרה של הממשק List:


מכיוון ש List הוא Immutable, הגדירו את המבנה הגנרי <out E> - וכך ניתן לשלוף E או sbutypes של E בצורה בטוחה.


את Mutable List הגדירו בדיוק על הטיפוס E, כלומר: המבנה לא יקבל (הכנסה / הוצאה) - טיפוסים ששונים מ E. זה לכאורה "פחות גמיש" אבל זו הגנה חשובה בפני מקרי-קצה בעייתיים.

אם ג'אווה (ליתר דיוק: ה JVM) היה תומך ב reified generics, כאלו שנושאים metadata ב runtime - ההתעסקות הזו הייתה נחסכת מאיתנו. זה המחיר ששילמו בג'אווה 5 על מנת לספק generics עם תאימות לאחור לקוד ישן יותר.


ה Variance בקוטלין הוא declaration-site variance, כלומר: כזה שנקבע בשלב ההגדרה - כמו ב  < List<out E שראינו למעלה. הקומפיילר "קשר" את הטיפוס E (או בנים שלו) למופע הרשימה - ואין צורך להצהיר על זה יותר.

בג'אווה ה variance הוא use-site variance, כלומר יש מגדירים את ה variance על השימוש - על המתודה. למשל.
הנה המתודה ()addall של המחלקה Collection:


הגדרתי עליה שאני מצפה לרשימה אחרת c המכילה את E או בנים שלהם - כי אני מתכוון רק לצרוך אותם.

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

מה עושים בקוטלין?



Type Projections


בקוטלין ניסו לצמצם שכפול קוד, ולכן ברירת המחדל היא declaration-site variance, אך אם צריך - ניתן גם להשתמש ב use-site variance. הנה דוגמה:


ל SomeStructure קשור טיפוס T כלשהו - אבל אני יכול להחליט שבפונקציה copy אני מצפה למבנה של T או supertypes שלו - לקריאה בלבד.  הדוגמה הייתה עובדת גם אם SomeStructure היה קשור ל <in T>.

אמנם MutableList קשור לערך מדויק (כפי שראינו למעלה), אבל כאן מדובר בסוגי ה MutableList שיכולים להישלח לפונקציה, כיחס ל T הקשור למחלקה. אין כאן קשר להגדרה של MutableList עצמו.

שימו לב שלא קשרנו טיפוס חדש T על המתודה (כפי שניתן), אלא השתמשנו בטיפוס T הקשור למופע המחלקה.


אם אני רוצה לוודא בפונקציה אם טיפוס מסוים הוא מבנה גנרי מסוג מסוים אני יכול לשאול:


if (x is Collection<*>) ...

הכלי הזה נקרא star projection והוא מקביל להגדרה ?out Any וגם in Nothing.




Reified Generics in Kotlin



היינו רוצים reified generics - אך החלטות design של ה JVM לא מאפשרות זאת.
בקוטלין, בכל זאת, התירו שימוש ב reified generics בפינה קטנה, שעשויה להיות שימושית לפעמים.

כאשר יש פונקציה שהורינו לקומפיילר לעשות לה inline - הקומפיילר יכול לאפשר בה שימוש ב reified generics - כאלו שיהיו זמינים ב runtime. למשל:

  1. כאשר הערך T קשור לפונקציה, מה יותר טבעי מלבדוק אם משתנה מסוים הוא מאותו הסוג?
    1. אופס! ... T קשור רק בזמן קומפילציה ואז הוא נמחק. הוא לא זמין ב runtime ולכן לא ניתן לבצע reflection: הקומפיילר פשוט לא יכול לנתח איזה ערך יישלח בזמן הרצת התוכנה.
  2. כאשר אני מגדיר את T כ reified - הקומפיילר יודע לבצע את האנליזה המתאימה כאילו יש לי את המידע ב runtime.
    1. זה יכול לעבוד רק על פונקציה שהיא inline.

לא ניתן לקרוא מקוד ג'אווה לפונקציה שהוגדרה כ reified: בכל מקרה הפונקציה היא inline והקומפיילר של ג'אווה לא ימצא הגדרה של פונקציות inline ב class files.




ולקינוח...


זוכרים את הביטוי המורכב של ()Collection.min בספריה הסטנדרטית בג'אווה? - בואו נוודא שאנחנו מבינים אותו, עד הפרט האחרון.


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

  1. ראשית קושרית בסוגריים משולשים טיפוס או טיפוסים ל scope של הפונקציה.
    1. האם יש טעם להצהיר T extends Object? זה לא מיותר?
      1. לכאורה כן: ההגדרה <T> שקולה ל <T extends Objects> כשהיא מופיעה לבדה.
      2. כאשר יש הגבלות (&), אם לא נצהיר על Object, ה erasure יתבצע להגבלה (Comparable) - שחסרה כמה מהתמודות של Object. בקוד הזה החליטו לקבוע erasure ל Object (שהוא גם Comparable שאליו קשור טיפוס לא ידוע שהוא supertype של T).
  2. ערך ההחזרה של המתודה (...)min הוא T. פשוט מאוד.
  3. שם הפונקציה.
  4. רשימת הפרמטרים. במקרה שלנו אנו מקבלים Collection אחר, של איברים לקריאה בלבד - שהם T או subtypes של T.

נראה פשוט, לא?



סיכום


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

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


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



---


[א] שווה לציין:


יום ראשון, 3 בספטמבר 2017

Dropwizard internals: Bootstrapping flow

הפעם, במקום לכתוב פוסט - תרמתי ישר לתיעוד של פרוייקט Open-Source, בשם Dropwizard.

Dropwizard הוא פרוייקט ש"מדביק ביחד" כמה ספריות Java מוכרות, בכדי ליצור lightweight HTTP container - עבור מיקרו-שירותים בג'אווה (או כל שפת JVM אחרת):



הנה החלק שתרמתי - על ה Internals של המערכת: כיצד מתבצע ה bootstrap של אפליקציית Dropwizard:

יש המשך...


קישור למסמך בגיטהאב

התוספת תצא בתיעוד הרשמי של גרסה 1.2 שתשוחרר בקרוב.

אם אתם מכירים מישהו שעובד עם Dropwizard - אנא שתפו איתו :)


מקור שם הספריה, אגב, הוא הקומיקס הזה:




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



יום חמישי, 17 באוגוסט 2017

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

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


אחד החלקים המתקדמים בשפה (באופן טיפוסי בשפות-תכנות?) הן היכולות לייצר DSL - כלומר: Domain Specific Language.
DSL היא "תת-שפה" המשמש בחלק מהמערכת לתיאור יעיל יותר של Domain מסוים. למשל: תחביר לייצור JSON, לגישה לבסיס הנתונים, לניהול חוקים עסקיים, תיאור UI, וכו'.

כאשר שפת-התכנות היא "נוקשה", היכולת להגדיר DSL היא דיי מוגבלת - וה DSL יוצא רב במלים, ועמוס לעין.
הנה דוגמה ל DSL של ספריית Camel כל גבי שפת ג'אווה - להגדרת routing של הודעות. אפשר להסתכל על DSL כ" API נוח יותר, שמושרשת בו הסמנטיקה של הדומיין":
FluentInterface - המעט שג'אווה יכולה להציע

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

ראינו את ה DSL המוגבל של ג'אווה,  ו DSL ... גרוע (ודמיוני) של Enterprise Java. כיצד נראה DSL יותר מרשים?

יוצרי קוטלין ראו מה שפת Groovy מאפשרת בתחום ה DSL... ראו והתרשמו. הנה דוגמה:
הם החליטו שהם רוצים גם!
לקוטלין היום שורה של יכולות, שלא נופלת מאלו של שפת גרובי. להזכיר שאנחנו מדברים על DSL שהוא עדיין statically typed - כלומר, התחביר יעבור בדיקה ע"י הקומפיילר.

מעוז סמלי של יכולות ה DSL של גרובי הוא כלי הבילד Gradle - מפתחי ג'אווה רבים (ברובם אנדרואיד?) משתמשים ב DSL של Gradle בכדי להגדיר build scripts פשוטים, ויעילים - תוך כדי שהם נהנים מבדיקת-התחביר של הקומפיילר.

אם ה API של Gradle היה מבוסס על שפת ג'אווה - לא היו מצליחים להגיע תחביר כ"כ פשוט ומינימליסטי.
לפני כשנה Grade החלה לתמוך בקוטלין כשפת ממשק First Citizen ל Gradle.
השימוש בקוטלין כשפת הממשק ל Gradle עדיין לא נפוץ, אך קהילת הקוטלין בקרוב תעקוף את קהילת הגרובי בגודלה - ודברים עשויים להשתנות

מעוז חשוב אחר של יכולות ה DSL של קוטלין היא ספריה לאנדרואיד בשם Anko - להגדרה של layouts (שלא ע"י XML).

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

הנה רשימת "יכולות ה DSL" העיקריות של קוטלין:


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


ברוכים הבאים לעולם היפה והמתעתע של הגדרת DSL 😄





Infix Functions 


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


אפשר להפעיל את המתודה foo על האובייקט A בתחביר רגיל (1) - או מקוצר.
התחביר המקוצר אפשרי רק כאשר הפונקציה מוגדרת כ infix, ו modifier של infix ניתן להוסיף לפונקציה רק כאשר היא מקבלת פרמטר בודד.

מתי זה שימושי?
זה יותר שימושי על טיפוסי בסיס: String, Boolean, וכו'. הנה דוגמה מ KotlinTest:


אבל... טיפוסי הבסיס הם לא שלנו - הם שייכים לשפה או לספריות הסטנדרטיות. מה עושים?

כאן באה לידי ביטוי יכולת חשובה של השפה בשם Extension Functions.


Extension Functions


מחלקת בסיס כמו String היא מאוד שימושית להרחבה - אבל היא חלק מהשפה או הספריות הסטנדרטיות.

קוטלין הושפעה מיכולת של שפת #C בשם Extensions Methods - ויצרה יכולת דומה הנקראת Extension Functions. אפשר פשוט להרחיב מחלקה, מבחוץ - מבלי "לפתוח" אותה:


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

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

  1. הנה הפעלה של extension function ל String - בתחביר הרגיל.
  2. הנה הפעלה בתחביר ה infix. רצף הקריאה הוא קולח יותר.
  3. נשלב את הקריאה לפונקציית למבדה.
  4. ניתן לשרשר קריאות infix אחת על השנייה.
    1. הרחבתי את האב הקדמון Any להכיל מתודה thenAdd - שתהיה זמינה לכל אובייקט בשפה.
    2. לא כל מה שאפשר להגדיר כ "DSL" הוא באמת קריא יותר: זו דוגמה לרצף קריאה מבלבל, שכנראה רק מי שהגדיר את ה DSL - יבין...

כדי לא לבלבל, כדאי להקפיד על הכללים הבאים בעת הגדרת infix functions:
  • על שם הפונקציה להיבחר בקפידה - עבור רצף הקריאות (readability flow). לא לבלבל ולא להפתיע.
  • אם אתם מתכננים לשרשר קריאות Infix נסו שהטיפוס יישמר, ולא לעבור בין טיפוס א' (Boolean) לטיפוס ב (מחרוזת) - כמו בדוגמה (הרעה) למעלה.
  • אל תזרקו exceptions מתוך infix functions.
    • infix function נראית כמו מילה שמורה בשפה - והמשתמש לא יצפה לאפשרות של exception (כפי שהוא לא מצפה ממילה שמורה בשפת קוטלין)

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

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

יש. הנה הם לפניכם:

  1. יצרנו מחלקה עם המתודה ()foo - ואז הרחבנו אותה. "מי תיקח"?
  2. בקוטלין יש כלל מאוד ברור: member תמיד קודם ל extension function
    1. אתם אולי יכולים לראות של ()C.foo - יש warning ב IDE שאומר: הפונקציה הזו מוחבאת ע"י ה member - ולעולם לא תוכל להיקרא. 
  3. עכשיו יש לנו 2 מחלקות היורשות זו מזו: B יורשת מ A. לכל אחת - הרחבה שונה לפונקציה foo.
  4. הכלל בקוטלין הוא שה resolving ל extension function הוא סטטי: ממשק האובייקט יקבע איזה פונקציית הרחבה להפעיל בפועל - ולא טיפוס האובייקט בפועל.
    1. הנה דוגמה: הפונקציה ()printFoo מצפה ל A (ממשק), אך מקבלת מופע של B (טיפוס האובייקט בפועל). מכיוון שה resolution הוא סטטי - בודקים אם ל A יש פונקציית הרחבה, ומכיוון שיש - מפעילים אותה.
  5. כאשר אין הרחבה לטיפוס / ממשק נתון - מחפשים בעץ ההיררכיה את המחלקה הראשונה שיש לה הרחבה מתאימה.

ניתן להגדיר extension functions גם על nullable types - מה שמאפשר להפעיל את הפונקציה גם כאשר הערך של האובייקט הוא null:


autocast הוא מנגנון של השפה, שלאחר שביצענו בדיקת null ושללנו את האפשרות - הקומפיילר יזכור זאת ולא "יטריד" אותנו יותר עם בדיקות האם הערך עלול להיות null. זו הסיבה שבשורה האחרונה אפשר לקרוא ל ()toString, מבלי להגן על האפשרות שערכו של האובייקט (מטיפוס ?Any) - הוא null.


Extensions, extensions, extensions


Extension Properties


ניתן להרחיב לא רק פונקציות של אובייקט, אלא גם תכונות של אובייקט.
עבור השמירה על ה encapsulation, קיימת מגבלה שלתכונה הזו לא יהיה  backing field:


כלומר: לא ניתן להוסיף initializer ולאתחל את הערך של extension property.
כן ניתן להגדיר את התכונה כ var, ולהוסיף לה setter.







Extension Functions הן כלי חשוב, אבל הן גם מועדות לשימוש-יתר.

מה יותר נחמד להוסיף עוד ועוד הרחבות לאובייקטים קיימים, לייצר "DSLs", ולהשתמש ב"יכולות המתקדמות" של השפה? אבל:

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


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

fun String.toJson()

מול

fun UtilsClass.toJson(string)

  • שאלה טובה היא: האם סביר שבעתיד יוסיפו את פונקציית ההרחבה שלי למימוש הסטנדרטי? האם ההרחבה שלי באמת כללית ומשרתת יפה מופעים שונים מהמחלקה?
    • אם התשובה היא "לא" - כנראה שיש פה מימוש ספציפי שלכם, ולא כדאי להשתמש בפונקציית הרחבה.
    • דוגמה חיובית היא למשל ההרחבה kotlin-jackson. לא דימיוני ש Jackson ירחיבו את ספריית הבסיס לתמוך כמו שצריך באובייקטים של קוטלין, וההרחבה הזו היא שמימושית לכולם.
  • כאשר אנו רוצים להרחיב אובייקטים בג'אווה / ספריות של ג'אווה לתמוך יותר טוב בשפת קוטלין (למשל: nullable types, הגדרת אופרטורים) - יותר הגיוני להשתמש בפונקציות הרחבה.
  • כאשר מחלקה היא גדולה ומורכבת - נסו ליצור אבסטרקציה חדשה, ולא להעמיס עוד הרחבות על המחלקה.
  • אם המחלקה היא שלכם - נסו להוסיף לה members, ולא הרחבות.

בקיצור: פה אתם משמשים ככלי העיקרי לבחירות מושכלות ובעלות רגישות.




Lambda Extension Functions (או: Lambda with receivers)


היכולת של פונקציות הלמבדה, לא לציין במפורש פרמטר יחיד, ולהראות כמו קינון - הוא בעל פוטנציאל DSL-י:


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

את היכולת הזו משלימים עם היכולת להגדיר receivers לפונקציות למבדה - מה שהופך אותן בפועל ל extension (lambda) functions:

  1. נפתח את הדוגמה בהגדרת מחלקה בשם RoutingMap. בשלב הזה - זהו בעצם alias. דמיינו שיש יותר.
    1. נ.ב: אם באמת הכוונה שלנו היא רק להציע alias - הצורה האידיומטית (idiomatic) לעשות זאת בקולטין היא בעזרת המילה השמורה typealias:
    2. מה שנחמד ב typealias הוא שאפשר להשתמש בו מול מחלקות שהן final - שלא ניתן לרשת אותן, כמו String או Int.
  2. אני יכול להגדיר DSL, בעזרת הגדרת פונקציה שמקבלת כפרמטר פונקציית למבדה.
  3. הנה ההפעלה: הפונקציה route:
    1. יוצרת RoutingMap.
    2. מפעילה את הלמבדה ("צריכת התוכן" ב DSL)
    3. רושמת את ה routes במקומות השונים, או במקרה שלנו - מדפיסה את ה map.
  4. בעזרת פונקציית למבדה עם receiver - הקוד יכול להראות נקי יותר.
  5. הנה ההפעלה של הפונקציה (route(2:
    1. יוצרת RoutingMap.
    2. שימו לב להבדל: בזכות ה receiver - בעצם הלמבדה מרחיבה (משמשת כ extension function) את מחלקת ה RouteMap - ולכן אנו פשוט מפעילים את הפונקציה של המחלקה.
      כמובן שההרחבה הזו טובה רק ל scope של פונקציית ה route2 - ולא מעבר לה.
    3. רושמת routes / מדפיסה.
  6. התחביר של פונקציית הלמבדה עשוי להראות מעט מוזר -  בואו נפרש אותו:
    1. התחביר הוא בעצם : <Receiver Type>.(<Param types>) -> <Return Type>
    2. לדוגמה: String.(Int, Int) -> Int
      אנו מרחיבים את המחלקה String, בעזרת פונקציית למבדה שמקבל שני פרמטרים מסוג Int ומחזירה Int.
    3. בד"כ / בפונקציות למבדה "רגילות" - פשוט אין Receiver.
    4. בדוגמה שלנו - פשוט אין לפונקציה פרמטרים.


רוצים להקדם קצת יותר ב DSL? - הנה שתי תוספות שניתן לעשות:
  1. אפשר להחליף את השם set לשם נוח יותר: למשל: addRoute - ע"י הוספת פונקציה ל RoutingMap שרק עושה delegation ל set.
  2. אפשר אפילו להרחיב את הביטוי כולו כדי שהיה "בשליטתנו". בדוגמה הבאה (הזריזה) החלפתי את ה Map ל ArrayList של Pairs משלי כדי להגדיר את respondWith עליו.
    1. יכולתי להרחיב את Any - אבל זו פרקטיקה רעה למדי. דמיינו אלו תקלות יכולות לקרוא שמישהו מוצא ב autocomplete פונקציה בשם שנראה לו הגיוני.
    2. מצד שני, אני לא בטוח שהפתרון של MyPairs הוא מוצלח גם כן: ויתרתי על יכולות ה hash של ה HashMap. כאמור: זו לא המלצת מימוש - רק משחק ביכולות של שפת קוטלין.




הרחבה אחרונה: אני רוצה לבנות מבנה מקונן, בו יש type safety ל response.

בדוגמה הבאה שיניתי כמה שמות - לצורך קריאות הקוד.
MyPair הוא בעצם RouteEntry. עברנו את השלב להסביר שהוא מחזיק בעצם זוג משתנים.
RoutingRegistry הוא לא באמת Map - אז לא נכון להצהיר עליו ככזה.
את HttpStatus - המחלקה המקוננת המתארת את ה response, יצרתי כ data class. ב RouteEntry דרסתי את toString בעצמי.

בסופו של דבר, הגענו לקוד יותר "DSL-י". כזה שאולי היינו שמחים לעבוד איתו להגדרת routes ב Web Framework:




Operator Overloading


Operator Overloading היא היכולת להגדיר התנהגויות של Operators (+, -, ==, וכו') למחלקות שלנו. למשל:


  1. בחרתי במחלקה דיי פשוטה: data class. 
  2. האופרטור + הוא בעצם המימוש של הפונקציה plus, והאופרטור * - של הפונקציה times
  3. ההפעלה של a+b היא דיי ברורה, אבל מי החליט מהי מחרוזת כפול מחרוזת?
    1. פה מתגלה הסיכון הגדול של שימוש ב Operator overloading: בניית סמנטיקה לא-צפויה.
    2. בכלל בהגדרה של DSL, לסמנטיקה ולמינוח המדויק - יש חשיבות רבה. בדריסת אופרטורים - על אחת כמה וכמה: חשוב מאוד לבחור סימן שיתאר התנהגות צפויה, גם ליישות =! self, גם למי שלא קורא את מימוש האופרטור.


בניגוד לסקאלה, לא ניתן להגדיר בקוטלין כל סימן אפשרי כאופרטור.
אולי אפשר להבין ש אבא + אבא = סבא, אבל מה זה לעזאזל אבא £ אבא, או אבא ^_^ אבא? (כל אלה אפשריים בסקאלה).

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



Destructing Operator


אופרטור מיוחד אחד של קוטלין הוא ה destructing operator ("פירוק המבנה"?).

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


נתחיל במילה הקצת מוזרה to המשתמשים בה להגדרה inline של Maps:


הא! זה "כולה" infix function, שעושה הרחבה (extension) לכל טיפוס (?Any) שייקרא A, עם פרמטר יחיד מכל סוג (?Any) שיקרא B - ומחזירה Pair של שניהם.
השורה:

val x = null to null

היא אם כן, ביטוי לגיטימי השומר במשתנה x אובייקט Pair עם שני nulls.
פשוט!

מה משמעות התחביר (char, index) אומר?
זו פעולת destructing declaration המציבה ערכים בשורה של משתנים (v1, v2, …​, vn) באופן הבא:

v1 = <T>.component1()
v2 = <T>.component2()
...
vn = <T>.componentn()

לדוגמה:


מאיפה מגיעות הפונקציות ()component1 ו ()component2?

זהו פשוט operator overloading שנעשה במחלקה Pair, מה שנקרא deconstructing operator.

עבור data classes - קוטלין תייצר את האופרטורים הללו בצורה אוטומטית, מה שיאפשר לנו תחביר יתר נחמד.


Invoke


אופרטור מעניין נוסף ואחרון שכדאי להזכיר או אופרטור ה ()invoke, המאפשר להשתמש באובייקט שלנו בתחביר הפעלה של פונקציה:


לא לבלבל: times3 הוא אובייקט - לא פונקציה, אבל אני עדיין יכול להפעיל אותו במה שנראה כהפעלה של פונקציה.

פונקציות למבדה בקוטלין, מיוצגות בעצם כאובייקט Function המממש אופרטור ()invoke. מכיוון שזה אובייקט, ניתן להעביר אותו לפונקציה כפרמטר, לשמור אותו בצד, וכו'.



סיכום


צללנו ליכולות ה DSL של קוטלין. מקווה שיצאתם מזה בשלום :)

אני רוצה לסיים בדיון קטן ב StackOverflow שנתקלתי בו, המייצג לדעתי היטב את הנושא:

>> בחור אחד שאל אם יש בקוטלין משהו דומה ל (rand(0..n של רובי, ביטוי המחזיר מספר רנדומלי מהטווח.
>> פתרון יפה שהציעו לו התבסס על extension function למחלקת ה CustomRange של קוטלין:


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

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

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


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