2019-09-14

האם תואר אקדמי במדעי-המחשב הוא עדיין רלוונטי?

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

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


התוכניות הנחשבות (בארה"ב) ללימודי מדעי-המחשב. המעוזים שיהיו האחרונים של התואר האקדמי?


הפתרון הטוב ביותר, לטעמי, בשלב זה הוא שהמדינה תשנה את תארי ה B.A או B.Sc להיקף של שנתיים לימוד במקצועות הנדרשים בתעשייה (ואפשר גם להשאיר "תואר B.A / B.Sc מורחב" בן 3 שנים - למען תאימות אפשרית לעולם, כל עוד זו לא תהיה ברירת המחדל).

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

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

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

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

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

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


אבל אולי בעצם "לימודים אקדמיים הם לעומק - ולימוד במקום העבודה הוא ספציפי לצורך המיידי, ללא עומק"?
אני רוצה להתייחס לטיעון הנפוץ, ולא-מדויק הזה:
  • ה"עומק" הוא הרבה פעמים שם קוד לתאוריה לא מעשית, או "לבסיס" כ"כ low level שהוא פשוט לא רלוונטי לרוב הגדול של העוסקים במקצוע. למדנו באוניברסיטה תקשורת והתמקדנו בפרוטוקולים הנמוכים (שכבה פיסית עד IP), אבל כמעט כל הנדסת התוכנה היא סביב HTTP כאשר רק מעטים מתעסקים באמת ב IP (שלא לדבר על שכבות נמוכות יותר). אין בעיה היום לפתוח את האינטרנט וללמוד מהו ARP או איך נעשה Routing ב Ethernet.
  • גם כשלמדתי באקדמיה נושאים מסוימים - את העומק האמיתי שלי השלמתי בתעשייה. לפעמים התאוריה האקדמית הייתה מנותקת מדי מהמציאות בכדי להיות שימושית. לקחתי קורס מורחב בבסיסי-נתונים, אבל את העקרונות החשובים שלהם - למדתי רק בתעשייה. כנ"ל לגבי מערכות הפעלה, ונושאים נוספים.
  • חוק תכנותיקה אחד קובע שחצי מהידע שלנו בתחום יהפוך ללא רלוונטי כל עשור. בכל מקרה, עלינו ללמוד כל הזמן - ויעיל יותר להתמקד בלמידה לאחר שבחרנו תחום ואנו יכולים לבחור את הידע הספציפי. יש הבדל גדול בין כתיבת מערכות (System/Infrastructure), לכתיבת מערכות נתונים (Enterprise Systems), כתיבה ב Frontend או ב Backend. שום "לימודי עומק" באקדמיה - לא "יצילו" אותנו מהצורך בלמידה מתמדת.
  • בני-אדם קולטים ידע טוב יותר מתוך צורך ממשי. כל הבניית התוכן בצורת "מבואות ויסודות" - הוא פשוט לא צורת הוראה יעילה. הדרך הנכונה היא להתחיל בלימוד משהו מעשי - ורק בהמשך להגיע ליסודות ולמבואות.
  • יותר ויותר מקומות עבודה היום יתנו לכם היום את החופש ללמוד ולהתפתח ע"פ בחירתכם, וינגישו לכם גם לימודי-עומק. תפקידם של הלימודים האקדמיים כ"מספקי העומק" הולך ומתפוגג גם מהבחינה הזו.

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


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



הזמנה שקיבלתי, לעשות תואר Master אונליין בשנה. שימו לב שזו אוניברסטית Salford ולא Stanford ...


מה הידע הנדרש?


מה באמת מהנדס תוכנה ממוצע צריך לדעת כיום?
  • שפת תכנות אחת לפחות (עדיף מאוד שתיים, בכדי לקבל פרספקטיבה!) - יש בתואר, למרות שלרוב לא מגיעים במהלך התואר לעומק משמעותי בשפה.
  • תרגול בכתיבת קוד - יש בתואר, אך לא מספיק. אין בעצם כמעט פידבק משמעותי על הקוד שנכתב - ולכן זו במעט למידה ב"ריק" [א].
  • מבני-נתונים וסיבוכיות - מכוסה היטב (עם מקום קל לשיפור). זהו נושא חשוב, אבל בשל הספריות העשירות היום - שימוש מעשי בידע הופך לפחות נפוץ עם השנים (בתקופת ה ++C - היה צורך כמעט-יומיומי בידע הזה).
  • בסיסי נתונים, תקשורת - מכוסה בצורה חלקית עד סבירה, תלוי במוסד. פעמים רבות אלו קורסי בחירה - וחבל, כי רוב המהנדסים יידרשו לידע בתחומים הללו.
  • בדיקות אוטומטיות - נראה שמכוסה הצד הטכני היבש, ופחות המתודולוגי. זה עדיין תחום שלומדים אותו במקום העבודה.
  • מקביליות - כנראה מכוסה, לרוב באופן תאורטי מדי. גם זה ידע חשוב, אם כי לא בשימוש יומיומי נפוץ.
  • דוגמאות למבנים מעניינים בתוכנה, ארכיטקטורות, דפוסי-עיצוב (Design Patterns) - מכוסה ברמות שונות. מהנדסים צעירים נוטים בצורה גורפת להשתמש בדפוסי-עיצוב באופן לא מושכל ומוגזם - ולסבך את המערכת.
  • עקרונות של הנדסת תוכנה - DRY, POLA, High Cohesion/Low-coupling, SOLID/GRASP, וכו׳ - לרוב מכוסים בצורה בינונית עד סבירה, עם דגש חסר פרופורציות ל DRY (העיקרון הקל ביותר להסביר).
  • קריאות ופשטות של תוכנה - העניין בהחלט מוזכר, אך גם בהחלט לא נלמד. רוב או כל הקוד שהסטודנטים כותבים לא דורש תחזוקה, ולא זוכה לפידבק משמעותי. שוב: עבודה ב"ריק".
  • התנהלות והתארגנות אישית - כיצד לארגן את העבודה בצורה הגיונית, ניהול פרויקטים בסיסי, התנהלות ארגונית, כיצד להשפיע על אנשים אחרים, וכו'. לתואר אקדמי אין נגיעה לעולם העשיר הזה (למרות שיש כאן לא מעט ידע תאורטי שניתן ללמוד).
  • נושאים טכניים מגוונים שונים כמו: סטטיסטיקה בסיסית, מערכות הפעלה, חומרת המחשב, וכו' - פעמים רבות נלמד במהלך התואר, אם כי מהסיכומים היה נראה לי שהמיקוד לא כ"כ מעשי. בהחלט אלו נושאים שאפשר להשלים מאוחר יותר. רוב הנושאים הללו ישמשו רק חלק מהמהנדסים - במהלך חייהם המקצועיים.
התנהלות והתארגנות אישית הוא כנראה הנושא הגדול והחסר ביותר למהנדסים צעירים. חלקם דווקא מגיעים עם רמת התנהלות לא רעה, ידע שספגו בבית או בחיים עוד קודם למקום העבודה הראשון. לכאורה אין ציפייה למיומנויות או ידע בתחום מתואר אקדמי, וכך הבוגרים - תמיד עומדים בציפיות או אפילו מפתיעים לטובה.

קריאות ופשטות של תוכנה היא ככל הנראה נקודת החסר הגדולה ביותר של מהנדסים צעירים בצד הטכני. בעצם הם עדיין לא מהנדסים - הם עדיין מתכנתים. אחרי 3-4 של לימודים, הם עדיין לא חוו הנדסת תוכנה. כלים תאורטיים שנתנו להם (DRY, Design Patterns) - משמשים בשנים הראשונות לפגוע בקריאות ופשטות התוכנה. הכל מכוונה טובה, כמובן.



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


אז מה עושים?


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

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

====

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

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

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


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

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

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

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

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


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

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


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


---

[א] ריק הוא ריק גם אם אינו מוחלט.
[ב] לא. הם לא באמת נדרשים בכדי להבין את החומר. אם רק היו מתאמצים קצת - היו מוציאים אותם מהמשוואה.

2019-07-29

על פרדיגמות תכנות, ומה ניתן ללמוד מהן בשנת 2019? - חלק ג׳


פרדיגמת התכנות פונקציונלי


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

פרדיגמת ה FP, בצורות כאלו ואחרות קיימת משנת 1957. בשנות ה-60, בשל כך שצרכה יותר זיכרון ומשאבים (שהיו אז מאוד מוגבלים) - היא הייתה בגדר רעיון מעניין, אך לא מעשי. בשנות ה-80 היא כמעט הפכה למיינסטרים - אך שפות כמו Basic ו C, שהיו מבוססות על תכנות פרוצדורלי, הפכו פופולאריות וקצת השכיחו אותה.

מה שהחזיר אותה לזירה לפני עשור בערך, היא הדאגה מריבוי ה cores במעבדים. רבים חזו שיידרש שינוי פרדיגמות משמעותי בעולם התוכנה - הרי עשרות שנים היו רגילים לחשוב בעיקר על Execution Thread יחיד, ופתאום אנו עוברים לעולם מרובה-cores (תכנות HPC, ו UI - כן חשבו על כמה threads, כבר לאורך שנים רבות).

אני זוכר את הדיבורים, לפני עשור או עשור וחצי, על כך שאוטוטו - לא נוכל לכתוב קוד יותר כפי שכתבתנו. שכל מתכנת יצטרך להתמחות ב Fork-Join או Patterns דומים של מקביליות, אחרת הקוד לא יהיה יעיל. אחד התוצרים של הדאגה הזו הייתה להחזיר את התכנות הפונקציונלי לשיח בתעשייה: כאשר עובדים עם מבני-נתונים שהם Immutable - קל הרבה יותר לכתוב קוד שירוץ בצורה מקבילית.

עשור עבר, מספר ה cores במעבדים אכן גדל, (אם כי בקצב מתון מהמדובר) - ובעצם עבור רוב המתכנתים מעט מאוד השתנה. ישנם מעבדים עם 20+ ליבות, אך הם משרתים בעיקר בצד השרת (ולא כ Desktop App) - ושם ריבוי cores משמש לשירות הרבה בקשות במקביל - כל אחד על thread. זה המצב ברוב הגדול של המקרים.

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




לפעמים אנו נוטים להסתכל על FP ו OO כבלעדיים (mutual exclusive) - כאילו עלינו לבחור רק בפרדיגמה אחת מהשתיים. יתרה מכך: לפעמים אפילו מתחיל דיון ״מי יותר טוב?״ דיונים שהרבה פעמים כוללים אי-דיוקים משמעותיים, לכל הפחות. זו גישה נפסדת ומטופשת: יש לנו כאן שני רעיונות טובים שמשתלבים היטב - ולנסות להפריד אותם, ולהוקיע אחד מהם - היא דרך טובה להיות פחות מקצועיים ויותר ילדותיים. די כבר עם ״תכנות פונקציונלי יותר חכם ומתקדם מ OO״ או ״תכנות FP הוא אקדמי ולא מעשי״. זה מטופש!

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

גם בשפות ״פונקציונליות טהורות״ (כלומר: שפות בהן ניתן דגש רב על רעיונות של FP) - יש אלמנטים של OO. למשל, ב Haskell יש הכמסה ברמת המודול (מודול לרוב הוא בגודל של קובץ בודד, והוא מקביל במידה מסוימת למחלקה בשפות OO) - וזו תוספת חשובה מאוד לשפה. ב Haskell יש גם ריבוי-צורות (Polymorphism), מן הסתם ברמת הפונקציה - שזה המבנה העיקרי בשפה.

הכמסה + ריבוי-צורות, ובלי הורשה (השנויה במחלוקת) - אפשר לרגע להתבלבל ולחשוב ש Haskell היא בכלל שפת OO מודרנית?

בקיצור: אין סיבה שלא נאמץ גם ספרות וגם שירה - הם לא מתחרים זה בזה, ולא צריך באמת לבחור. כנ"ל לגבי OO ו FP.



אז מה שפות FP חידשו לנו?



Functional Style

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

ובכן, קשה להשוות FP לשפות דקלרטיביות כמו SQL. ב SQL אנו מתארים כוונה עם מעט מאוד הַכוונה - ו"מישהו" דואג שהיא תתבצע. בשפות פונקציונלית מתארים כוונה עם הרבה מאוד הַכוונה - וברור מאוד כיצד הדברים עומדים להתבצע. אני חושב שיש אכן טעם לעשות הפרדה בין Imperative Style לבין Declarative Style לבין Functional Style - שהוא משהו באמצע.

אפשר לומר שמקום לומר למעבד מה לעשות צעד אחרי צעד (אימפרטיבי) - ב FP אנחנו אומרים לו מה לעשות שלב אחרי שלב 😀

ב Functional Style מחליפים (בגדול) משפטי if במשפטי filter, לולאות במשפטי map או רקורסיות, ובמקום להשתמש במשתנה result שיצבור את התשובה של הפונקציה - משתמשים ב fold.

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

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

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



שרבטתי דוגמה לפונקציה, שכנראה תהיה פשוטה יותר לכתיבה ב Imperative Style.
אפשר (כמובן) לכתוב אותה גם ב Functional Style - אבל בסיכוי גדול הקוד יהיה מסובך (״מתוחכם״) וקשה יותר להבנה:

הוספתי הערות מטא, שמציגות אלו אלמנטים בפונקציה יהיה מורכב יותר לבטא ב Functional Style.
אני מקווה שההערות לא מקשות מדי על קריאת הקוד...



Immutability

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

Immutability משמע שהערכים ומבני הנתונים שקבענו לא ישתנו מרגע שנוצרו. מנקודת מבט של שפות אימפרטיביות, זה אומר שכל הערכים / השדות בשפה הם final, const, או איך שזה נקרא בשפה הספציפית.

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

דוגמה משפת קוטלין בה למבני-נתונים (Data Classes) יש פונקציה מובנה בשם copy אותה יוצר המהדר, המאפשר בקלות להעתיק את מבנה-הנתונים תוך כדי שינוי ערכי שדות מסוימים. במקרה הזה, העתקנו את אובייקט ה Payment - אך שינינו את שדה התאריך.

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

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

Immutability גורר שכל שינוי state כולל העתקה - ויש לכך מחיר בבצועים, ולפעמים מחיר ב boilerplate code - תלוי בשפה, אך בעיקר כאשר עושים שינויים עמוקים במבני-נתונים מורכבים ומקוננים.

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



No Side Effects

רעיון מרכזי ב FP הוא רעיון ה "Pure Functions", פונקציות טהורות - עם אפס השפעה על הסביבה.

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

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

עד כאן - תכנות / הנדסת תוכנה. הצורך בפונקציות צפויות הוא צורך ליבה, ואנשי-תוכנה מנוסים מקפידים מאוד לברור את שמות הפונקציות - כדי לתאם ציפיות בצורה הטובה ביותר האפשרית (POLA). למשל: תחילית try לשם פונקציה שעשויה לא לבצע את הפעולה.

חלק משמעותי מההפתעות הגדולות שקורות בהפעלת פונקציה נובעות משינוי state של אובייקט אחר (קריאה לגיטימית לכאורה, לממשק הציבורי של האובייקט), שליחת מייל או הפעלת תהליך אחר לא צפוי. אלו בעצם side effects, ו side effects משמעותיים.

בפרדיגמת ה OO כלי משמעותי למניעת הפתעות ממש מאותו הסוג הוא Encapsulation: את ה state הפנימי של האובייקט ניתן לשנות רק במסלולים ״המקובלים״, מה שמצמצם משמעותית הפתעות. זה חשוב מאוד - אבל לא תמיד מספיק. הפתרון של FP הוא לחתור לכך שכמעט כל הפונקציות במערכת יהיו pure functions - פונקציות שבשום אופן לא משנות משהו מחוץ ל scope הפנימי שלהן, ולא מסתמכות על שום דבר מעבר לארגומנטים שנשלחו להן - וכך אנו מגבירים מאוד את הסיכוי שהן יהיו צפויות. הפעלה של Pure Function, מספר פעמים ובזמנים שונים - תמיד תציג אותה תוצאה, כאשר נשלחים לה אותם הארגומנטים.

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

האם זה לא Functional Decomposition? אותו דפוס שלילי שדיברנו עליו בפוסט הקודם?
ישנם הבדלים: Functional Decomposition קלאסי פעל על State גלובלאי בו ביצעו כל הזמן שינויים - וזה מאוד לא צפוי.
כן יש פה עניין של חלוקת אחריות מפוזרת ולא ברורה בפוטנציה, ולכן תראו הרבה פעמים שמערכות המושפעות מ FP עדיין משתמשות בחלוקה למודולים (או מחלקות, בשפות OO+FP) והקפדה על סידור הגיוני של המערכת.
אם אני לא יכול למצוא בקלות קוד שעושה משהו, סביר שאכתוב אותו מחדש => כפילות קוד.

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

יש עוד כמה יתרונות חשובים לפונקציות טהורות:
  • מאוד טבעי לכתוב להן בדיקות יחידה. כלל חשוב שהתווסף ב TDD עם השנים הוא לחלץ ״לוגיקה עסקית טהורה״ לפונקציות / מחלקות נפרדות - כדי לכתוב בדיקות-יחידה סופר-יעילות, וללא Mocks. זה בדיוק מה שקורה ב Pure Functions.
  • פשטות במקביליות. כאשר כותבים קוד מקבילי, state משותף הוא נקודת כאב מרכזית: אנחנו צריכים לנהל אותו, לנעול אותו, וגם להימנע מהשלכות שליליות של locks כגון deadlocks ופגיעה במקביליות - וזה לא קל. דווקא קל מאוד למקבל פונקציות טהורות - אם כי לא תמיד זה מוביל לביצועים הטובים ביותר (בשל ההעתקות הרבות הנדרשות, ובשל אי-ניצול של memory locality).
  • Memoization - אם פונקציות הן טהורות, אזי ניתן בקלות ובבטחה לעשות caching לתוצאות החישוב. אם פונקציה היא טהורה, ומבני-הנתונים הם Immutable אז ניתן להחליף את הקריאה לפונקציה בתוצאת החישוב המוכנה מראש - מבלי לשנות בכלל את התוכנה. העיקרון הזה נקרא גם referential transparency. כמובן ש caching הוא לא תמיד יעיל, למשל - אם יש פרמוטציות אפשריות רבות להפעלת הפונקציה.



כמה שאלות ותשובות:
  • האם באמת אפשר לכתוב מערכת רק מפונקציות "טהורות"? לא. כל מערכת חייבת Input/Output בכדי שתהיה לה משמעות. כלומר: יש את הבעיה הקטנה שמה שלקוחות המערכת צריכים ממנה - הם side effects. אז אי אפשר בלי side effects.
    • ״במערכות על טהרת ה FP״ מנסים למקסם את אחוז הפונקציות הטהורות במערכת. כאשר מדובר ב batch processing (סוג בעיה שהרבה פעמים מפתחים עם FP) - אז פשוט אפשר להעביר את ה state בין הפונקציות מההתחלה עד הסוף - הוא לרוב מספיק קטן בכדי לא לאבד עליו שליטה / ששכפול שלו יהיה Overhead גדול מדי.
      • גישה אחת, היא ליצור Queues שיקבלו הוראות על שינוי state (למשל: שינוי בבסיס הנתונים) ואז באמת יהיו כמה פונקציות, שמאוד ברור מי הן - שרק מבצעות "side effects". שולפות הודעות מה Queue - ומבצעות את השינויים.
        • Actors הוא מודל מקביליות שתומך בגישה הזו.
      • גישה אחרת, היא לאגור את ה state המשתנה במעין ״טרנזקציה״: הפונקציות יתרמו לשינוי state, אבל הוא לא יחול על לנקודה מאוד ברורה בקוד ובזמן. הגישה הזו עוזרת להתמודד עם עניינים כמו מקביליות / racing conditions של עדכון ה state או state מורכב שקשה לעדכן ע״י הודעות.
    • שווה לציין שלא רבות המערכות שקמות ״על טהרת ה FP״. זה עובד נחמד ב Batch Processing (היכן שזה פשוט) - אבל זה יכול בקלות להסתבך. 
    • גישה שנראה לי שהולכת ותופסת תאוצה היא פשוט לסמן בצורה ברורה (coding conventions, annotations, וכו׳ - למשל IO Monand) אלו פונקציות אינן טהורות (impure) וכך לתאם ציפיות. אלו הפונקציות להיזהר מהן. כמובן שלא תמיד אפשר לכתוב מערכת שרוב הפונקציות בה הן impure ואולי יותר נפוץ, בגישה OO+FP מעורבת, לסמן פונקציות שהן pure - על מנת שיהיה אפשר לסמוך עליהן יותר.
  • באם באמת כל הפונקציות שלא משנות state / ניגשות לנתונים חיצוניים - הן טהורות? לא בדיוק. למשל: כתיבת הודעת לוג מתוך פונקציה - הופכת אותה "רשמית" ללא טהורה. גם כאן, הפרגמטיות היא במידתיות. מבחינתי, אפשר להחשיב גם פונקציות עם side effects זניחים כ "פונקציות טהורות". אם נתפלסף, אזי גם לפונקציה שמחברת שני משתנים יכול להיות side effect. למשל: הוא גורמת לעדכון caches בתוך ה CPU. בואו לא נגזים.
  • מדוע אומרים שפונקציות אסינכרוניות הן לא טהורות?  נראה לי שזה ענין של קורולציה. בד"כ מפעילים פונקציות א-סינכרוניות עבור פעולות I/O (שזה side effect ברור), ולכן בדרך כלל הן לא טהורות. לא נראה לי שיש משהו "לא טהור" בפונקציה מעצם כך שהיא מורצת בצורה א-סינכרונית.


Function Composition

בגישת ה FP מדברים על "higher order functions״ כלומר - פונקציות שמקבלות פונקציות בתור פרמטר - על מנת להרכיב פונקציות מורכבות יותר.

למשל: רוב הפונקציות המאפשרות Functional Style הן צורה פשוטה של Function Composition. הפונקציות map או filter מקבלות פונקציה (״למבדה״) שבעזרתן הן עושות את הפעולה.

זה נחמד, שימושי, ומועיל. אפשר כך לפרק פונקציה מורכבת לפונקציות פשוטות יותר, זה מאפשר קוד Reuse, וזה מאוד תומך ב״דרך החשיבה הפונקצינלית״ שזו סוג של חשיבה מתמטית. הרי Function Composition (כמו רעיונות שונים של FP) - הוא רעיון שמגיע ישירות מהמתמטיקה.

Function Composition הוא גם מקור לצמיחת סיבוכיות, ופיזור קוד.

אין לי דוגמה לשלוף, אז שוב שרבטתי משהו מהיר (ושוב: בשפת קוטלין):


הנה דוגמה ל pure function שמצליחה להיות לא צפויה ולא ברורה.
כבר אמרנו שבכל כלי טוב - ניתן להשתמש בצורה גרועה?

קריאת קוד הפונקציה לא מלמד אותי מה היא עושה - הכל תלוי בפונקציות ששולחים לה כארגומנטים.
גם אם אגש מה call-site (הנקודה בקוד בה מפעילים את הפונקציה) ואראה מה שולחים לה -  לא תהיה ברורה התוצאה, כי הקשר בין customerFilter ו selectionLogic הוא לא ברור-מאליו, בטח לא עם ״התוספות״ ששתלנו, כמו בדיקת ה balance ואם הלקוח פעיל.

האם מישהו כותב פונקציות כאלו?!
כן. זה קורה. מתוך אהבה ל FP אנשים מזהים דפוסים חוזרים בקוד - ומוציאים אותם ל composer functions שאמנם חסכו כמה שורות קוד כפולות - אך הפכו את הקוד למאתגר להבנה ותחזוקה.

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


טיפוסים גמישים

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

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

עוד טיפוס שימושי מאוד בשפות פונקציונליות הוא ה Enum. ה״פילוסופיה של FP״ היא לא להשאיר דברים ליד המקרה בזמן הריצה. לא להשתמש ב nulls, ולא לעבוד עם exceptions - הקוד שנכתב צריך לכסות את כל המקרים בזמן הקומפילציה.
על Haskell נאמר ש״אם הקוד מתקמפל - אזי התוכנה עובדת״ - בדיוק בשל אימוץ הגישה הזו.

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



סיכום


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

אומר זאת שוב: אין עניין של בחירה ב OO או FP. שמעתי דיונים כאלו כמה פעמים - והם פשוט מטופשים. זה כמעט כמו לדון מה יותר טוב: HashTable או Vector/ArrayList?
את שניכם אתם רוצים שיהיו בסט הכלים שלכם, וכל דיון צריך להיות למקרה הספציפי והבעיה שאתם מנסים לפתור. יתרה מכך, השילוב ביניהם - הוא בד״כ האופציה הטובה ביותר. דיון רלוונטי הוא מתי, ועד כמה להשתמש בכל אחד.

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

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

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


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