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

הפעם אני רוצה לדבר על Interoperability בין קוטלין וג'אווה.

 

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

מתישהו… במקרי הקצה – זה יגיע.
משהו בג'אווה לא יאהב משהו בקוטלין (או אולי ההיפך – אבל זה פחות נפוץ).

כיצד null יכול לצוץ לו בקוטלין ללא הודעה מוקדמת?

 

כבר כמה פעמים נשאלתי את השאלה: "האם אפשר לרשת מחלקת ג'אווה בקוטלין? קוטלין בג'אווה?"

בוודאי שאפשר! אחרת לא הייתי אומר ש interoperability ביניהן כ"כ מוצלח.

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

ערבוב של קוד קוטלין וג'אווה לצורך רצף הקריאות. במציאות כמובן שהקוד ישב בקבצים נפרדים.
  1. יצרנו מחלקה מופשטת A בשפת ג'אווה.
  2. הרחבנו את המחלקה בג'אווה A – בעזרת מחלקה בקוטלין B.
    1. מכיוון שברירת המחדל בקוטלין היא מחלקה final – עלינו להגדיר אותה כ open ע"מ שקוד הג'אווה יוכל לרשת את המחלקה C.
  3. ואכן הרחבנו את המחלקה בקוטלין B בג'אווה, ללא בעיה. כל שפה שומרת על הקונבנציות שלה (במידת האפשר)
  4. הממ… ה IDE מעיר לי פה משהו: 
Not annotated method overrides method annotated with @NotNull 

מה זה?
אני לא רואה Annotation בשם NotNull@ בקוד.

מה? java.lang.NullPointerException? – אבל אני כותב בקוטלין!?

 

בכדי להבין מה קורה, נחזור שלב אחר אחורה – למחלקה KotlinB.

במחלקה הזו דרסנו את המתודה ()getHelloMessage שהוגדרה בג'אווה.
ערך ההחזרה של המתודה שהוגדרה בג'אווה הוא String, אבל מה זה אומר עבור קוטלין: String או ?String, אולי?

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

והנה השימוש שלה בקוטלין:

ה IDE מסמן לי שערך ההחזרה של המתודה הזו הוא !String.

אין טיפוס כזה בקוטלין, וטעות נפוצה היא להניח ש !String הוא ההיפך מ ?String – כלומר: String שהוא בהכרח לא null.

מה שבאמת ה IDE מנסה לומר לנו הוא שהוא לא יודע אם ה String הוא null או לא. אני חושב שתחביר כמו [?]String היה יכול להיות יותר אינטואיטיבי.

מתכנני שפת קוטלין החלו עם הגישה הדפנסיבית והניחו שערך ההחזרה של מתודה המגיעה מג'אווה הוא null – אבל גישה זו הפכה את הקוד שמשתמש בג'אווה (ובד"כ יש הרבה כזה) – לקוד מסורבל מלא בדיקות בפני null. קוד לא יפה:

other?.java?.object?.always?.might?.be?.null()
 

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

זה נוח, אבל גם יכול לגרום לשגיאות בלתי צפויות.

הנה דוגמה מהחיים:

jdbi הוא פריימוק רזה (וחביב) הכתוב בג'אווה, ומאפשר גישה לבסיס הנתונים.
אופן השימוש בו הוא להגדיר interface או abstract class עם מתודות ומוסיף להן annotation עם השאילתה שיש לממש.
jdbi, בעזרת reflection, מג'נרט (בזמן ריצה) אובייקט DAO שמממש את הממשק שהגדרתי. התוצאה היא אובייקט שמבצע את השאילות שהגדרתי ב annotations. קוד עובד.

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

…עד הרגע שאני מפעיל את השאילתה עם job_id שלא קיים – וחוזר לי null.
מתישהו אני "חוטף" NullPointerException.

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

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

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

עלי לשים לב שמדובר בשאילתת SELECT, ויותר מכך: עם תנאי WHERE – ולכן יכול לחזור null.
עליו להנחות את הקופיילר בהתאם – ואז הכל יתנהג כצפוי.

במקרה הזה קוד הג'אווה אינו בידי – הוא של ספריית צד-שלישי.
אבל מה אם זה היה קוד שלי? מה יכולתי אני לעשות בקוד הג'אווה בכדי למנוע את הסיטואציה?

JebBrains (החברה מאוחרי קוטלין ו IntelliJ) סיפקה annotations לג'אווה שיכולים להנחות את ה IDE, מתי צפוי null ומתי לא ייתכן null. הנה דוגמה:

השימוש ב annotation מסיר מה IDE את הספק:

ואז הוא יכול להגן עלי.

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

באופן דומה, אגב:

(Mutable)List

הוא סימן שה IDE מספק שמשמעו: רשימה שייתכן שהיא mutable, וייתכן immutable. הקומפיילר לא מסוגל להגיע למסקנה בעצמו.

הנה דוגמה לביטוי מורכב:

  • הרשימה ו/או האיברים בה עלולים להכיל ערך null.
  • הרשימה עשויה להיות mutable או לא.
קוד הג'אווה מאחורי המתודה ()getStrings הוא זה:
 
 
מה שמוביל אותנו לעניין נוסף שכדאי להכיר:
כאשר מתודה בג'אווה נקראת ב naming של JavaBeans, כלומר: ()getXxxx או ()setXxxx – קוטלין מתייחסת אליהם כתכונה בשם xxxx.
 
הנה הקוד בקוטלין שקורא לקוד הג'אווה הנ"ל:
 
 
אתם רואים שהשלפנים (getters) שכתובים בג'אווה נראים בקוטלין כמו תכונות לכל דבר.
מכיוון ש true היא מילה שמורה בקוטלין, יש לעטוף (escaping) אותה בגרש מוטה.
 
באופן סימטרי, תכונות (properties) שהוגדרו בקוטלין כ yyyy יופיעו בקוד הג'אווה כמתודות ()getYyyy ו/או ()setYyyy.
 
 
כדרך אגב, יכולת ה escaping של שמות בקוטלין – מאפשר לתת שמות קריאים יותר לפונקציות של בדיקות:
 
 

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

 

חשיפה מתוכננת

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

אם יש לי תכונה בשם yyyy בקוטלין (מה שטבעי בקוטלין), הגישה אליה תהיה בעזרת getYyyy ו setYyyy – מה שטבעי בג'אווה.

כדי להדגים את הבעיות והפתרונות הנפוצים שנובעים מכך, יצרתי (בכוונה) מחלקה הכתובה בקוטלין – שלג'אווה יהיה קשה מאוד "לאכול":

אאוץ. אאוץ!!

הנה רשימת בעיות:

  • כאשר אני קורא לתכונה now מג'אווה – שם הפונקציה מופיע כ ()getNow, ומסיבה כזו או אחרת אני רוצה להשתמש בשם now כ field.
  • המילה transient היא מילה שמורה בג'אווה – אך לא בקוטלין. אי אפשר לקרוא לפונקציה הזו מתוך ג'אווה, ואין escaping בג'אווה המאפשר להשתמש בשמות שאינם תקינים בשפה.
  • אני לא יכול ליהנות מהערך ברירת המחדל של המתודה repeat. אין קונספט של default value בג'אווה – ולכן אני נדרש לשלוח את שני הפרמטרים בכל קריאה. בריבוי קריאות – זה יכול להיות מעצבן!
  • יצרתי companion object על מנת "לחקות" מתודות סטטיות בג'אווה – אבל הדרך לקרוא ל foo היא באופן: ()KotlinProducer.Companion.foo. מסורבל!

מה עושים?

הנה הפתרון, מבוסס annotations – אך עובד:

 

JvmOverloads היא הנחיה להשתמש ב default values על מנת לג'נרט מופעים שונים של פונקציות בקומבינציות השונות, מה שנקרא בג'אווה Method Overloading. אני מניח ששאר ה annotations הן self-explanatory.

הוספתי גם דוגמה לשימוש ב extension function. איך משתמשים ב extension functions מתוך ג'אווה?!

הנה קוד הג'אווה שמשתמש בקוד הקוטלין, "בהנאה":

 

דיי אלגנטי, מלבד ה Extension Function שבאמת לא משתווה לשימוש בקוטלין.
יש לציין את שם המחלקה (כל קובץ של קוטלין מתתרגם למחלקה עם Kt בסופה) נקודה שם המתודה – כאשר הפרמטר הראשון הוא האובייקט עליו אנו רוצים לפעול.

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

 

סיכום

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

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

ה interoperability בין ג'אווה וקוטלין פשוט עובד!

יצא לראות לא מעט קוד קוטלין (צד-שרת) שעובד:

  • בצורה אינטנסיבית עם ספריות של ג'אווה.
  • ספריות ותיקות, שנכתבו לג'אווה – עוד לפני שקוטלין הייתה מעניינת.
  • ספריות שמבצעות reflection והורשה לקוד הקוטלין שנכתב (למשל: JDBI, Guice, או Jackson שמקודד עשרות רבות של מחלקות ל json ובחזרה לקוטלין)
  • והעבודה הייתה בסה"כ חלקה מאוד!
    • במקרים מעטים היה צורך / או היה יפה יותר להשתמש בכלים שסיפקתי בפוסט הזה.
    • במקרים מעטים נאלצנו לכתוב קוד "java-like" בקוטלין, על מנת שדברים יעבדו. עם הזמן צצו wrappers לקוטלין שהקלו על הדברים, ואפשרו להשתמש בסגנון "קוטליני" בחופשיות.

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

ג'אווה, גרסה 3 – להתעורר מהתרדמה הדוגמטית

שפת ג'אווה. קהילה אדירה, אימוץ משמעותי וארוך שנים…

– אבל גם שפה שהגיל ניכר בה:

  • בעיקר, אבל לא רק: תחביר verbose – מרבה-במילים, שכבר לא מקובל בשפות חדשות (למשל: swift, go, וכו') – ומסיבה טובה.
  • Generics מבוסס "מחיקת טיפוסים" – שהופך את הדברים לקשים יותר, עניין שצץ מחדש בשימוש ב Steams. מחיקת הטיפוסים נעשתה במקור על מנת לשמר תאימות לג'אווה 1.4. למי אכפת היום מקוד שכתוב בג'אווה 1.4?!
  • רעיונות טובים שהתבררו כשגויים לחלוטין לאורך השנים – כמו checked exceptions.
  • פרימיטיביים מול אובייקטים ו auto-boxing.
  • חתימת זיכרון גדולה למדי של ה JVM.
  • ועוד…
הוותיקים שביננו בוודאי זוכרים, ששפת ג'אווה התחילה בתור ״Project Oak״ – כשפה ל"טלוויזיות חכמות" איי שם בשנות ה-90.

בהמשך, הסבו אותה ל"ג'אווה" כשפה ל Applets – המספקת את היכולת לכתוב קוד שירד ברשת – וירוץ בדפדפן. JavaScript נקראה על שמה של ג'אווה – ובאה להציג גרסה "קלילה" יותר לג'אווה, בתחביר ובצריכת המשאבים.

ואז הבינו שהיכולת של "Write Once, Run Anywhere" (או בקיצור: WORA) היא יכולת שמצוי הפוטנציאל שלה הוא לא בטלוויזיות חכמות (יחסית לשנות ה-90), ולא בדפדפנים – אלא דווקא בצד השרת.

כתוב את קוד השרת על מחשב ווינווס (היכן שקל לפתח) – אבל הרץ אותו על Unix ("שרתים אמיתיים") או Linux ("שרתים זולים – למי שמתעקש").

היום, "שרתים רציניים" מריצים Linux, ופיתוח הכי נוח לעשות על Mac (ה Linux Subsystem על "חלונות 10" מנסה קצת לאזן את התמונה), אבל היתרון שב WORA – חשוב כבעבר.

את התובנה הגדולה זו – הציגו בכך שהכריזו (ובצדק) שג'אווה עברה לשלב הבא: Java 2!

נוספו מאז ה J2EE, ה JVM נפתח כקוד פתוח – והשאר היסטוריה.

מקור: https://plumbr.io/blog/java/java-version-and-vendor-data-analyzed-2017-edition

לעשות שינוי

חוק תיכנותיקה ידוע, אומר שלא ניתן לבצע שינויים גדולים – ללא disruption.

קהילת ג'אווה, שהייתה קנאית ל Backward Compatibility מאז ומעולם – יודעת זאת.
יכולות כמו Generics או Streams הגיעו לשפה באיטיות – ונעשו בהן פשרות מוחשיות.

פרויקט ה Jigsaw ("פאזל", ה Java Module System) החל בשנת 2005. הוא תוכנן להשתחרר כחלק מג'אווה 7 (שהתעכבה ל 2011) – אבל שוחרר עכשיו, רק בג'אווה 9 – עשור ושלוש שנים מאז שהחלה העבודה עליו.

האימוץ של גרסה 9 של ג'אווה עתיד להיות אטי – וכואב. מכיוון שג'אווה 9 איננה backward compatible, אנשי התוכנה יהססו לאמץ אותה גם מסיבות פסיכולוגיות, ולא רק סיבות טכניות גרידא.

אני לא טוען שהתאימות לאחור של ג'אווה היא טעות. אותה תאימות לאחור, מתישה ומכאיבה – היא אחת החוזקות של שפת/קהילת ג'אווה.

בשפת ג'אווה עצמה – היכולת לבצע התקדמויות משמעותיות הולכת וממצה את עצמה משנה לשנה. לפחות: התקדמויות ללא disruption. ההתאוששות מחוסר התאימות-לאחור של ג'אווה 9 צפויה לארוך כמה שנים – מה שמפחית מאוד את הסיכוי לשינויים דרמטיים בשפה בשנים הקרובות.

הפתרון הברור הוא להציג את שפת Java גרסה 3: גרסה מחודשת מהשורש, ולא תואמת-לאחור – של השפה.

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

הרבה מימים אפשר לייצר עם המספר 300

חבלי מעבר

המעבר מ"גרסה 2" ל"גרסה 3" – אמור לצלצל לכם מוכר. מזהים משהו?

לא? … נסו שוב.

אי אפשר (כך נדמה לי) שלא לחשוב על המעבר ההיסטורי של שפת פייטון מגרסה 2 לגרסה 3 – וההשלכות של המעבר הזה.

המעבר היה מתבקש, מכיוון שהיה כמו תכונות "ישנות" של פייטון שכבר היו "בלתי-נסבלות":

  • מחרוזות לא היו Unicode (אלא ANSI?)
  • התנהגויות בעייתיות של הטיפוס הכ"כ בסיסי int:
    • אופרטור החלוקה / היה מעגל את התוצאה למספר שלם – מקום נפוץ לשגיאות.
    • int היה כמו BigInteger של ג'אווה – שזה נהדר, חוץ מהעובדה שקוד פייטון מרבה להשתמש בספריות ++C/C (עבור יעילות – זה חלק מהמודל ההנדסי של השפה) – אבל אז ההמרה יצרה בעיות.
  • אופרטור השווה (=, , וכו') שהיה מופעל על טיפוסים שונים – היה משווה את ה id של האובייקטים, אם לא הייתה מוגדרת צורת ההשוואה – מקור לצרות אמיתיות.
בדרך הכניסו בגרסה 3 גם שיפורים שימושיים (super ללא פרמטרים, set literal, וכו'). המודעות לחשיבות התאימות-לאחור הייתה קיימת , ובהחלט נמנעו משינויים חשובים, אך לא הכרחיים. למשל: בנאי (constructor) בפייטון עדיין נקרא __init__, עוד מהתקופה שמפתחי השפה הניחו שרק הם זקוקים לבנאים – אבל משתמשי השפה לא יזדקקו להם. זהו שינוי מתבקש, אך לא "הכרחי" – ולכן לא נכלל בפייטון 3.

ראשי הקהילה תיארו זאת כך:

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

התוכנית לא כ"כ הצליחה: בסיסי קוד שברמת תחזוקה / שליטה בקוד גבוהה – היה ניתן להמיר במאמץ לא גדול – אבל המפתחים לא המירו את הקוד – כי ספריות רבות שהשתמשו בהן עדיין לא עברו לפייטון 3.

מתחזקי ספריות ותיקות כבר הפחיתו את ההשקעה בספריה – ולא היו מוכנים לעשות את המעבר. גם להם היו לפעמים ספריות אחרות שהם תלויים בהן – שלא ביצעו מעבר לפייטון 3. ה Distros העיקריים של לינוקס המשיכו להיות מסופקים עם פייטון 2 – וחוזר חלילה.

וכך – כל העגלה נתקעה. קריאה לקהילה לבצע את המעבר לא נענתה בחיוב, או לפחות לא בקצב מהיר. הוקמו אתרים כמו http://py3readiness.org ו http://python3wos.appspot.com שמדדו ועודדו – את האימוץ של פייטון 3.

פייטון 3 שוחררה ב 2008.

ב 2010 יצא עדכון 2.7 שהיה אמור להיות העדכון האחרון של פייטון 2 – אך הפך במהרה לגרסה הנפוצה ביותר של השפה.
בלית ברירה מפתחי השפה עשו downport לפיצ'רים רבים מפייטון 3 לגרסה 2.7 – על מנת להקל, בהמשך, על המעבר לפייטון 3.

מקוור: http://blog.thezerobit.com/2014/05/25/python-3-is-killing-python.html

ביקורת על המהלך – לא הייתה חסרה. אינספור דיונים התקיימו בעד ונגד פייטון 2/3. השאלה הנפוצה ביותר בפייטון באותה תקופה הייתה ככל הנראה: "אני מתחיל פרויקט חדש. האם להתחיל בפייטון 2 או פייטון 3?".

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

הגרסה הנוכחית של פייטון 3 היא גרסה 3.6 – וקראתי כתבה שטענה ש 80% מהקוד החדש נכתב כיום בפייטון 3.
95% מהספריות הנפוצות כבר תומך בפייטון 3, וחלקים מהן מפסיק לתמוך בפייטון 2 (דוגמה: Django). במצב הזה, כנראה – קיוו יוצרי פייטון להיות בשנת 2010. זה קרה "רק" 8 שנים מאוחר יותר.

"הגרסה הבאה של פייטון תהיה פייטון 8, והיא לא תתאם לאחור בכלום" – מתיחת 1 באפריל

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

מקור: https://snarky.ca/why-python-3-exists

אם הייתי האחראי העולמי לשפת ג'אווה, ובמיוחד אם אני עובד בתאגיד גדול (שמרן מטבעו) כמו אורקל – בוודאי (!!) שלא הייתי נוגע בתפוח האדמה הלוהט הזה שנקרא Disruption עמוק בשפה עצמה. הייתי משאיר את זה לבא אחרי – שיבוא עוד 10-12 שנים. הוא בטח יעשה את זה, לא?!

נכון, ג'אווה 9 שברה תאימות לאחור לג'אווה 8 – וזה לא עניין של מה בכך. בכל זאת, השינוי נוגע לניהול packages ותלויות – ולא לשפה עצמה. מכיוון שג'אווה היא strongly typed ו static – לא קשה ליצור כלים שיגלו וינחו בשינויים הנדרשים, להלן jdeps. כן צפוי קושי עם ספריות שעושות שימוש כבד ב reflection, ספריות כמו Hibernate או AspectJ.
שינוי בתחביר של השפה – הוא סיפור אחר לגמרי.

מה עושים? האם אין דרך בה ג'אווה תוכל להתחדש – וליישר קו עם שפות מודרניות?

יש פתרון!

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

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

במקרה שלנו: נספק שני סוגי סיומות קבצים:

  • java. – עבור ג'אווה 2.
  • java3. – עבור ג'אווה 3.
כל עוד התחבירים השונים יוכלו לחיות זה לצד זה – גם באותו הפרויקט, ולתקשר זה עם זה (להפעיל פונקציות / לקבל ערכי חזרה) ללא קושי מיוחד – הפתרון יעבוד. תמיכה ל Java.V2 יש לספק לעוד עשור, לפחות.
באופן זה אוכל לכתוב בג'אווה 3 – תוך כדי שאני ממשיך להשתמש בספריות שעדיין עובדות בג'אווה 2.
אם בפרויקט, אני צריך לכתוב קוד חדש באזורים הכתובים בג'אווה 2 – אני ממשיך לכתוב אותו בג'אווה 2.
באופן זה אני יכול לכתוב מודול חדש בתוך הפרויקט בג'אווה 3, ולהתחיל לחדש שאת הפרויקט שלי בהדרגה – מבלי שאני נדרש להמיר את כולו, ובמכה אחת – לג'אווה 3.

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

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

  • היכולת לצרוך פחות זיכרון (תהליך של ג'אווה מגיע לצריכת זיכרון של חצי GB – דיי בקלות) – הוא שיפור חשוב למיקרו-שירותים, ול FaaS (למשל: AWS lambda).
  • שמירת טיפוסי Generics בזמן ריצה (להלן "reified generics") – הוא שיפור מבורך שיתיר קוד מסובך במקרים רבים. הוא גם יאפשר לכתוב קוד שלא ניתן היום לכתוב בג'אווה.
מצד שני, שינוי ב JVM עלול להיות קשה וארוך לביצוע. לא נרצה לסיים במצב בו עלי להריץ שני תהליכים עבור Interoperatiblity: תהליך של JVM2 ותהליך של JVM3. אני אסיים עם יותר צריכת זיכרון ו overhead משמעותי (IPC) בתקשורת שבין התהליכים.
לכן, לצורך העניין – נסתפק בשיפורים שהם רק ברמת השפה עצמה. נעדיף את האפשרי – על "המושלם".

תוכנית עבודה

בואו נחשוב לרגע: כמה זמן לוקח לתכנן מחדש ולרענן את התחביר של ג'אווה?
בהתבוננות על שפות אחרות הייתי משערך שמאמץ שכזה (עד GA) יארוך כחמש שנים לגוף זריז ודינאמי, או 10 שנים לגוף מסורבל כמו גוף התקינה של ג'אווה.
אם נתחיל עכשיו, ונוציא את המטלה מידי ה Java Committee – אנחנו יכולים להגיע לשם כבר בסוף שנת 2023!

הבשורות המשמחות הן שתהליך כזה כבר החל בשנת 2011!

ג'אווה 3 כבר מוכנה ועובדת. יש עשרות אלפי מתכנתים שעובדים בה במשרה מלאה. היא production-ready ועם interoperability מוצלח למדי לג'אווה 2!
אני יכול להעיד זאת לאחר עבודה לאורך החודשיים האחרונים עם JavaV3 בסביבת פרודקשיין.

לא פחות משמח: חברת אורקל תמשיך לתמוך ולפתח את Java.V2 באהבה ובמסירות עוד שנים רבות. זה לא פחות חשוב – כי סביר שקוד Java.V2 עוד יחיה שנים רבות.

הקושי העיקרי של Java.V3 היא שהיא זוכה להתעלמות רועמת מצד קהילת הג'אווה הרשמית.
אצילות היא לא דרישה בעולם העסקים, ולהיפך: על אצילות (שפוגעת בשורה התחתונה / נכסים "אסטרטגיים") – הולכים הביתה.

אף אחד מקהילת הג'אווה הרשמית לא הולך לומר לכם ששפת קוטלין היא בעצם Java.V3.

ולכן, זה הזמן לקהילה החופשית לעשות את השינוי.

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

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

בכל מקרה, ל2 השפות הנ"ל הייתה ההזדמנויות להגיע ל adoption משמעותי – אך הן לא צלחו. מכיוון שהן תוכננו לפני עשור ויותר – הן כבר לא יהיו Java V3. לכל היותר – Java V2.5 שנראה עם פוטנציאל – אבל לא הצליח.

מקור: http://pypl.github.io/PYPL.html – בעת כתיבת הפוסט

קוטלין כן הצליחה ליצור spike של adoption – בעיקר על בסיס מתכנתי אנדרואיד.

בעקבות האימוץ הטבעי של קוטלין בקהילת האנדוראיד, וגם בשל הלחץ שהופעל עליה מצד קהילת ה iOS (מול Objective-C Java.V2 נראתה טוב, אבל בהשוואה ל Swift – היא כבר נראית עייפה ומסורבלת) – גוגל הפכה את קוטלין לשפה רשמית של אנדוראיד.

קוטלין היא שפה מודרנית בהחלט: תחביר פשוט, פונקציות הן 1st Class citizens בשפה עצמה, יש לה הגנה בפני nulls, שיפורים לספריות, extension functions, ועוד). קוטלין גם מאפשרת Interoperability מוצלח מאוד מול Java.V2, ויש לה IDE חינמי, הרץ על שלושת מערכות ההפעלה – עם תמיכה מעולה בשפה.

בקיצור:

קוטלין היא המועמדת הטובה ביותר לתפקיד Java.V3!

אני מוכן להעיד על כך מניסיון אישי. זו פשוט שפה טובה.

מה נותר? נותר לנו – הקהילה, לאמץ אותה!

אני באמת לא רואה סיבה מדוע לא לאמץ אותה בחום בצד-השרת. עשינו זאת בחברת Next Insurance – בהצלחה רבה!

השיפור בפריון וקריאות הקוד – הוא לא שיפור ש Java.V2 תוכל איי פעם להשיג – במגבלות שחלות עליה. מודל ה Interoperability למעבר בין גרסאות שפה – הוא מצוין. לא יהיה כאן שוד ושבר.

האם באמת מפתחי הג'אווה שבעו מחידושים, והם מסתפקים בזחילה לכיוון ג'אווה 9, או ב Spring Boot – כהתקדמות בסביבת העבודה שלהם?!

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

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

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

לסיכום (עודכן)

משפת ג'אווה כבר קשה לצפות לשיפורים משמעותיים, ובטח לא בעתיד הקרוב.
גרסה 8 של ג'אווה "סחטה" דיי הרבה שיפורים – מהמצב הנתון, וזו הייתה התבלטות לטובה. בשנים הקרובות – נראה שקהילת הג'אווה תהיה עסוקה בעיקר במעבר ל Java Module System. שיפור חיובי – אך הדרגתי. שיפור שאגב יחול – על כל שפות ה JVM.

בכלל, כמה וכמה חידושים ברמת השפה בג'אווה 9 נראים דומים בצורה מחשידה ליכולות שיש כבר בקוטלין: Stream API improvements? – כבר יש. private interface methods – כבר יש. REPL? – ברור שיש. גם default interface methods הגיעו לקוטלין לפני שהגיעו לג'אווה.
אני חושד שהיוצרים של קוטלין מסתכלים על ה JSRs המתגלגלים של ג'אווה, ומביאים אותם לקוטלין – פשוט יותר מהר.

אם אתם מפתחים ג'אווה רוצים לשפר את כלי העבודה שלכם, שיפור משמעותי – קוטלין היא אופציה ממשית וחשובה.

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

קהילת האנדרואיד אימצה את קוטלין בצורה "טבעית". למען ההגינות: כנראה שהיה שם לחץ גדול יותר מזה המונח על קהילת צד-השרת: קהילת האנדרואיד הייתה "תקועה" עם ג'אווה 6.

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

האם אני מפספס משהו? האם אתם יודעים להסביר מדוע מעט כ"כ אנשי ג'אווה (צד-שרת) מתעניינים באימוץ של קוטלין?

אשמח לתגובות 🙂

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

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

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 של ג'אווה (עדיין אפשר לכתוב את הקוד בקוטלין).

סיכום

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

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

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

אחד החלקים המתקדמים בשפה (באופן טיפוסי בשפות-תכנות?) הן היכולות לייצר 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. התחביר הוא בעצם : .() ->
    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 = .component1()
v2 = .component2()
vn = .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 מבחינתי, במיקרוקוסמוס: פוטנציאל יפה לקריאות טובה יותר של קוד, לצד סכנה לבלבל.

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

קוטלין (Kotlin) למפתחי ג'אווה ותיקים – חלק ד': עוד על מחלקות, אובייקטים, ועוד…

בואו נתחיל!

היררכיית הטיפוסים של קוטלין

בקוטלין, האב הקדמון של כל האובייקטים הוא Any – המקבילה של Object בשפת ג'אווה.

כך נראה אובייקט ה Any (השמטתי את ה comments):

אולי אתם שואלים את עצמכם היכן המימוש של המתודות? מדוע הוא לא הוגדר כ Abstract?
אני מניח שהמימוש נעשה ברמת ה runtime – ויש החרגה ברמת הקומפיילר למחלקה הספציפית הזו.

המחלקה Any מכילה גם כמה extension functions (החשובות שבהן: apply, let, run, also) – המוגדרות בקבצים אחרים. נדון בקונספט ה extension function ובמתודות הללו ספציפית – בהמשך הסדרה.

האם Any הוא באמת האב הקדמון של כל המחלקות בקוטלין?
לקוטלין יש את מערכת ה Nullable, שבעצם אומרת ש:

Any? = Any || null

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

ניתן לשרטט סכמה של היררכיית הטיפוסים בקוטלין בערך כך:

קשר לוגי משמעו שהקומפיילר מכיר בקשר, ואפשר באמת להתייחס לקשר בקוד, אך לא תמצאו קוד כזה בקוד המקור של קוטלין: אין קוד מקור למחלקה ?String היורשת מ ?Any.

הזכרנו כבר שבקוטלין אין פרימיטיביים, ולכן כל ה wrappers (כמו Int) – יורשים בעצם מ Any.

Nothing הוא מקרה קצת מיוחד – אותו גם הזכרנו: Nothing מתאר ביטוי שלעולם לא יסיים evaluation. למחלקים Nothing לעולם גם לא יהיו instances.

מכיוון שכל ערך יכול להיות מוחלף ב Nothing, אבל אף ערך לא יורש ממנו, נהוג לצייר את Nothing בתחתית ההיררכיה – אבל זה תיאור מטאפורי בלבד.

השימוש ב Nothing עוזר לסמן לקופיילר מקרים ש"הקומפיילר לא צריך לדאוג מהם". למשל, המימוש הבא של EmptyList (זהו ה pattern של NullObject – למי שמזהה):

מקור

הורשה בקוטלין

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

  1. עלי להצהיר על המחלקה open, אחרת תהיה לי שגיאת קומפליציה שתצביע על המחלקה SpecialPerson שיורשת אותה. מחלקות הן final בקוטלין, כברירת מחדל. זו גישה הגנתית המספקת פחות הפתעות מהורשה לא צפויה.
  2. כנ"ל לגבי פונקציה: כל הפונקציות הן final כברירת מחדל – ויש "לאפשר" לרשת אותן במפורש.
  3. חובה להגדיר override בפונקציה שדורסת מימוש של מחלקת-האב, בניגוד לאנוטציה Override@ בג'אווה – שהיא רשות.
  4. התחביר של הורשה הוא : – כמו ב #C. סוגריים נדרשים כדי לדעת אם להפעיל את בנאי-ברירת-המחדל של מחלקת האב – או בנאי אחר.

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

  1. הגדרתי את המתודה a כ final במחלקה SpecialPerson. לולא ההגדרה – הגדרת ה "open" מהמחלקה Person – הייתה ממשיכה הלאה במורד ההיררכיה.
  2. …ולכן – יש לי שגיאת קומפליציה בבואי לדרוס אותה.

מחלקות חתומות (Sealed Classes)

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

  1. אני מגדיר את המחלקה InsuranceResult כ sealed (במקום Open). זה יתיר רק למחלקות שהוגדרו באותו הקובץ (private scope) – לרשת ממנה.
  2. הנה יצרתי 2 מחלקות פשוטות. העימוד בא להדגיש את הקשר.
  3. שימוש נפוץ הוא בכדי להחזיר סט סגור של ערכים. למשל: פעולה של אישור ביטוח שיכולה להסתיים בהצלחה (תשובה מסוג אחד – Approved) או בכישלון (תשובה מסוג אחר – Denied).
    1. המחלקות Denied, Approved, ו InsuranceResult – הן public, וניתן להשתמש בהן מכל מקום.
  4. הנה הקוד שמקבל את התשובה, גם הוא – פשוט ואלגנטי.

מחלקות מופשטות (abstract), וממשקים (interfaces)

בקוטלין יש מחלקות מופשטות (abstract):

  1. עצם ההגדרה של מחלקה כ abstract אומרת ש:
    1. המחלקה יכולה להכיל members מופשטים (abstract).
    2. לא ניתן ליצור instances ממנה.
  2. הנה פונקציה מופשטת. פונקציה מופשטת היא "פתוחה" (open) בהגדרה.
  3. אפשר גם להגדיר פונקציות קונקרטיות – עם מימוש. הם יהיו "סגורות" כברירת מחדל.
  4. כשאני דורס פונקציה מופשטת, עדיין עלי להצהיר על דריסה בעזרת override.
  5. יש לי שגיאת קומפילציה, מכיוון וניסיתי לרשת פונקציה ממחלקה מופשטת, בלי המילה override. מוסיפים override – ויש שגיאה נוספת. מה הפעם?
    1. כברירת מחדל פונקציות (ומחלקות) הן final. עלי לשנות את ה פונקציה collect ב InventoryItem להיות open – ורק אז הכל יסתדר.

ממשקים בקוטלין הם דיי דומים לג'אווה – עם כמה הבדלים קטנים.

החל מג'אווה 8, ממשקים יכולים להחזיק מימוש קונקרטי למתודות.
יוצרי קוטלין טוענים שהם חשבו על הרעיון קודם (הושפעו מה Traits של סאקלה)! בכל מקרה – התוצאה דומה:

  1. הנה הגדרה של ממשק פשוט: יש בו פונקציה אחת מופשטת (ללא מימוש), ואחת קונקרטית – עם מימוש.
  2. בממשקים של קוטלין ניתן להגדיר תכונות, אבל ללא state (חייבים להגדיר getter, ולא יהיה  backing field מאחורי התכונה).
    1. שימוש אפשרי לתכונות על ממשק – היא כחלופה לקבועים על הממשק – שאין בקוטלין.
  3. הנה אני יוצר אובייקט שיורש מ Person ומממש את הממשקים Happy ו Shiny. סדר ההגדרה של הממשקים / מחלקת אב – לא משנה.
  4. אני מחויב לדרוס את המתודות המופשטות של הממשקים, כמובן – ולספק להן מימוש קונקרטי.
  5. מה קורה כאשר לכמה ממשקים שירשתי מהם, יש פונקציה קונקרטית עם חתימה זהה (בדוגמה שלנו: reflect)? כיצד לא נגררים לבעיות של הורשה מרובה?
    הפתרון של קוטלין אלגנטי: הקומפיילר מחייב אותי לממש מחדש את הפונקציה.

    1. שימו לב שאם החתימה היא שונה (פרמטרים שונים) – אז אין בעיה, ואני לא אחויב לממש מחדש.
  6. אם אני רוצה לפנות למימוש קונקרטי – אני פשוט קורא ל <super.<function name.
    שנייה! יש פה בעיה: הקומפיילר לא יודע איזה מימוש של reflect אני רוצה לקבל: של Shiny או של Happy?

    1. הפתרון הוא לבאר לקומפיילר מה אני רוצה, בעזרת התחביר הבא: (super.reflect(s 
      כשאני משתמש בתחביר הזה – הקומפליציה תקינה, וקריאה ל ()reflect של S.H.Person – תפעיל את המימוש הקונקרטי של Happy.
השאלה המתבקשת עכשיו, היא: מה ההבדל בין מחלקה מופשטת בקוטלין, לממשקים?
אם אפשר להגדיר פונקציות קונקרטיות על ממשק – מה בעצם הצורך במחלקה מופשטת? האם מחלקות מופשטות הן לא "מיותרות"?

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

  1. במחלקה מופשטת כל ה members הם final כברירת מחדל (כמו כל מחלקה) – בממשק הם open, ולא ניתן להגדיר אותם כ final.
  2. בממשק לא ניתן להגדיר members כ internal. ורק פונקציות קונקרטיות ניתן להגדיר כ private. בקטנה.
  3. מחלקה יכולה לרשת ממשקים רבים – אך רק מחלקה מופשטת אחת – זה כנראה ההבדל הכי חשוב.
  4. זה מעט מורכב: ממשק לא יכול להחזיק state, אחרת עלולים להיווצר קונפליקטים במימוש-מרובה.
    לכן, השפה מציבה כמה מגבלות על תכונות (properties) שהוגדרו בממשקים:
  1. בממשק אני יכול להגדיר val או var:
    1. val – ניתן להגדיר לו getter בלבד, או להשאיר את התכונה מופשטת (abstract).
    2. var – התכונה תמיד תהיה מופשטת. ניסיון להוסיף getter יגרור לשגיאת קומפילציה.
  2. מכיוון שהתכונות של הממשק Koo הן מופשטות – המחלקה המממשת חייבת לממש את התכונות הללו: x ו y (להלן override), ואז עלי לנקוט באחת מהגישות הבאות:
      1. אני יכול להציב בתכונות ערך התחלתי (מה שאי אפשר לעשות בממשק)
      2. אני יכול לממש getter/setter בעצמי.
        1. הערה: val הוא readonly – ולכן אין צורך ב setter.
      3. אני יכול לדרוס את התכונות ב primary constructor. הערכים שהוגדרו שם "יחביאו" את אלו שהוגדרו על הממשק.
כלומר: בסופו של דבר היתרון הגדול של הממשקים הוא ביכולת של מחלקה לממש מספר ממשקים, והשפה תגן עלי בפני קונפליקטים אפשריים. היתרון של מחלקות מופשטות הוא כנראה ההרגל (של מפתחי ג'אווה ותיקים) – והיכולת להגדיר state.

מחלקות מקוננות (Nested Classes)

בקוטלין יש 2 סוגים של מחלקות מקוננות:

  1. Minion היא מחלקה מקוננת של Father, סתם כך.
    כברירת מחדל מחלקה מקוננת היא "סטאטית" כלמר – מחלקה עצמאית לכל דבר שרק חיה ב namespace של המחלקה שעוטפת אותה.

    1. יוצרים אותה בנפרד.
    2. היא לא יכולה לגשת לתכונות / פונקציות של מחלקת האב.
  2. בצורה השנייה, משתמשים במילה השמורה inner, בכדי לחבר את המחלקות ברמת המופעים (instances)
    1. לא ניתן ליצור מחלקה פנימית ללא החיצונית. ביצירת מופע של המחלקה החיצונית – נוצר גם מופע, מקושר, של המחלקה הפנימית.
    2. אם יש התנגשות בשמות, ניתן להשתמש ב qualified this" expression" – להגיע למחלקת האב. דומה מאוד ל OuterClass.this בג'אווה.
      1. ניתן גם להשתמש ב qualified super, בתחביר ()super@Father.foo – בכדי לגשת לפונקציה של המחלקה ממנה האובייקט החיצוני יורש.

מה קרה ל static ובן-אל?

בקוטלין ביטלו את המילה השמורה static. אין קונספט של מתודות סטטיות במחלקה.
מצד שני, יש קונספט של singleton מובנה בשפה – בעזרת המילה השמורה object:

  1. בעזרת המילה השמורה object, אנחנו בעצם מגדירים מחלקה – ומייד יוצרים לה מופע.
    1. אובייקט הוא מופע יחיד (singleton). השימוש הנפוץ בו הוא ליצירת Factory או אובייקט שמכיל איברים סטטיים – המשותפים לכולם.
    2. ניתן להגדיר על האובייקט תכונות, ופונקציות – אבל לא בנאים. הרי: לא יוצרים אותו. המופע נוצר בעצם ההגדרה.
    3. ניתן לרשת ממחלקה אחרת / לממש ממשקים.
  2. אם היינו רוצים להגדיר קבוע בג'אווה היינו משתמשים ב static final. בקוטלין קיימת במילה השמורה const שמצביעה לקומפיילר שמדובר בערך שידוע בזמן קומפילציה. השימוש ב const היא הצהרה שזהו ערך סטטי שלא הולך להשתנות. המשמעות:
    1. אי אפשר להציב תשובה מפונקציה, קרי ()const val x = foo – זו שגיאת קומפילציה.
    2. אי אפשר להוסיף getter – הרי getter יוכל "לשנות את הערך, ע"י החזרה של ערך אחר", ולעולם בעתיד לא יהיה ניתן להוסיף getter. זו אולי התכונה בעלת הסמנטיקה העמוקה ביותר.
    3. אפשר לאתחל את התכונה רק ערכים פרימיטיביים (מספרים, מחרוזות), ולא אובייקטים – אחרת, ייתכן אי אפשר להבטיח שכלום לא ישתנה.
    4. בפועל: מאחורי const לא יהיה backing field.
סיבה מרכזית שבגללה השתמשנו בג'אווה במילה השמורה static היא לצמצם מספר מופעים: אם יש לי קבוע מסוים במחלקה, שיש לה אלפי מופעים – לא ארצה לשכפל את הקבוע אלפי פעמים בזיכרון.
בקוטלין, ניתן להגדיר קבועים או פונקציות במרחב הגלובאלי, אולי בתוך אובייקט – בכדי לנהל את ה scope בצורה מסודרת יותר.
מה עושים כאשר אנו רוצים את ההתנהגות של ה static methods בג'אווה? לקרוא לפונקציה בלי לייצר מופע של האובייקט? לכך יש כלי בשם Companion Object:
  1. לצורך הדוגמה יצרתי אובייקט מקונן בתוך המחלקה.
    1. זו צורת עבודה לגיטימית. תחביר-ההפעלה הוא קצת יותר מסורבל, כי יש לציין את שם האובייקט.
  2. כאשר אני מגדיר companion object, אני יכול לפנות לפונקציות שלו, ישירות תחת ה namespace של המחלקה. ל companion object:
    1. אין צורך להגדיר לאובייקט שם.
    2. ניתן להגדיר רק companion object יחיד בכל מחלקה.
    3. ה companion object ייווצר בזמן שטוענים את קוד המחלקה – מה שמקביל את הפונקציות שלו ל static methods בג'אווה.
  3. ה annotation של JvmStatic@ היא רשות, והיא תגרום ל bytecode שנוצר לגרום לג'אווה להתנהג עם ה companion object בצורה דומה לאופן הטיפול בקוטלין.
    1. ללא ה annotation יש לקרוא בג'אווה: ()Log.Companion.createLogger
    2. עם ה annotation יש לקרוא בג'אווה: ()Log.createLogger
  4. אם המחלקה עושה shadowing למשתנה / פונקציה של ה companion object, אני יכול בצורה מפורשת לגשת ל member של ה companion object בעזרת השם Companion.

שונות

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

late init

לפעמים יש לי תכונה מסוג var שאני עומד להציב בה ערך בתהליך האתחול של המערכת – אך לא מהרגע הראשון.
הקומפיילר, המגן עלי בפני nulls – יצעק, ולא יוותר. "מה פתאום ערך לא מאותחל?!"
בואו נראה דוגמה קלאסית:

  1. הפתרון ה"פשוט" הוא להציב ערך null בשדה – עד האתחול ה"אמיתי".
    1. בקוטלין זה דיי מעצבן, כי אז עלי להפוך את כל הטיפול באותו משתנה ל nullable – התעסקות מיותרת.
  2. הפתרון: להוסיף modifier בשם lateinit, המורה לקומפיילר: "אחי, עלי! המשתנה הזה יאותחל לפני שיקראו לו". השימוש ב lateinit מאפשר קוד נקי יותר, ללא טיפול ב nullability.
    1. מצד שני: אם טעינו (והטענו את הקומפיילר) – תיזרק שגיאת …UninitilizedProperty.
      שם השגיאה, מכוון למהות הבעיה, יותר טוב מאשר לקבל סתם Kotlin)NullPointerException).
  3. כאשר מגדירים lateinit, השדה של התכונה (במקרה שלנו: repository2) ייחשף לקוד הג'אווה. זוהי החלטה תכנונית של מקסום התאימות לג'אווה.
    1. אפשר למנוע את חשיפת השדה ע"י ה annotation האופציונלי: field:JvmSynthetic@

ה runtime של קוטלין קובע ערך null בתכונות lateinit – לסמן לעצמו שהן עדיין לא אותחלו. לכן – לא ניתן להגדיר lateinit לתכונות מטיפוס שהוא Nullable.

גישה לאובייקט ה Class

בג'אווה ניתן לגשת למחלקה של אובייקט ע"י הקריאה object.class – זהו בעצם אובייקט מטיפוס <Class<T, הנוצר ב מנגנון ה reflection ומתאר את תכונות (metadata) המחלקה: שם ה package, שדות, וכו'.

ספריות שונות משתמשות ביכולת הזו, למשל: JUnit:

@RunWith(RobolectricTestRunner.class)

בקוטלין, ניתן לגשת לאובייקט ה reflection המתאר את המחלקה בעזרת הקריאה object::class – זהו אובייקט של ספריית ה runtime של קוטלין מסוג KClass (על הכוכבית – בהמשך הסדרה).
הוא מכיל מתודות ותכונות reflection של קוטלין, למשל companionObjectInstance – המחזירה מצביע ל companion object, אם קיים.

רוצים לקבל את אובייקט הג'אווה המתאר את המחלקה שלנו (אובייקט ה Class, שהוא בעל מבנה שונה מאובייקט ה KClass)? – קראו ב object::class.java.

ברוב הפעמים דווקא נדרש לאובייקט-המטא של קוטלין. למשל:

@RunWith(RobolectricTestRunner::class)

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

סיכום

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

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

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