הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

האתגר של הטווח הארוך

03/06/2023

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

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

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

הבאג הבא בדוקר קומפוז כמעט עבר לי מתחת לרדאר

02/06/2023

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

  redis:
    image: redis
    ports:
      - 6379:6379

אחרי שעתיים הרדיס התרסק ובלוג הופיעה הודעה דומה להודעה הבאה (רק עם תאריך אחר בהתחלה):

1:S 25 Jun 2021 00:48:12.902 * MASTER <-> REPLICA sync started

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

יכולים לראות מה הבעיה האמיתית שם?

המשך קריאה

לימוד מהיר זה אשליה

01/06/2023

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

it really doesn't take much time for a developer staying on top of these advancements in their area of interest to reach a level of familiarity that rivals those with decades of experience.

וגם במודגש-

Staying on top of a few chosen technologies allows anyone to become an expert in them rather quickly.

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

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

מה שמביא אותי לשתי מחשבות בעקבות הפוסט של קנט-

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

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

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

האק בפייתון: תלות מעגלית

31/05/2023

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

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

# File: a.py
import b

def hi(n):
    print("Hello from a")
    if n < 10:
        b.hi(n+1)

hi(0)
# File: b.py

import a

def hi(n):
    print("hello from b")
    if n < 10:
        a.hi(n+1)

אם נשמור את קטעי הקוד בקבצים בשמות a.py ו b.py בהתאמה ונריץ python a.py נקבל את השגיאה:

Hello from a
Traceback (most recent call last):
  File "/Users/ynonp/tmp/python/circular/a.py", line 1, in <module>
    import b
  File "/Users/ynonp/tmp/python/circular/b.py", line 1, in <module>
    import a
  File "/Users/ynonp/tmp/python/circular/a.py", line 8, in <module>
    hi(0)
  File "/Users/ynonp/tmp/python/circular/a.py", line 6, in hi
    b.hi(n+1)
    ^^^^
AttributeError: partially initialized module 'b' has no attribute 'hi' (most likely due to a circular import)

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

import a

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

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

האק מרושע שיכול לעזור במצבים כאלה הוא להעביר את ה import לתוך הפונקציה. אחרי השינוי המודול b.py נראה כך:

# File: b.py

def hi(n):
    import a
    print("hello from b")
    if n < 10:
        a.hi(n+1)

הפעלה חוזרת והכל עכשיו עובד:

Hello from a
Hello from a
hello from b
Hello from a
hello from b
Hello from a
hello from b
Hello from a
hello from b
Hello from a
hello from b
Hello from a
hello from b
Hello from a
hello from b
Hello from a
hello from b
Hello from a
hello from b
Hello from a
hello from b
Hello from a

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

בעיות מסובכות מדי

30/05/2023

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

ללמוד פיתוח ווב היא לא מאלה. גם ללמוד פייתון. או כל שפת תכנות אחרת.

להשתמש נכון ב git היא לא כזאת בעיה. אפילו לא לעשות ריבייסים.

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

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

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

לקוף יש בעיה

29/05/2023

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

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

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

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

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

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

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

והנה זה שוב

28/05/2023

הקטע הבא מועתק מתוך התיעוד של next.js:

import { cookies } from 'next/headers';

export default function AddToCart({ productId }) {
  async function addItem(data) {
    'use server';

    const cartId = cookies().get('cartId')?.value;
    await saveToDb({ cartId, data });
  }

  return (
    <form action={addItem}>
      <button type="submit">Add to Cart</button>
    </form>
  );
}

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

מצד שני בהנחה הסבירה ש saveToDb אכן כותב לבסיס הנתונים לשורה שמזוהה לפי cartId, קל לראות איך משתמש יכול לגרום לכתיבה לעגלות שאינן שלו, פשוט על ידי שינוי ה cookie בדפדפן בצורה ידנית. כל הבעיות הישנות סביב שימוש לא מאובטח בקוקיז עשויות לחזור ובגדול. שימוש ב Server Components בריאקט עלול לטשטש מחדש את ההבדל בין קוד צד שרת לקוד צד לקוח, ואיתו לשכוח את אילוצי האבטחה השונים של כל צד.

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

לצייר בתוך הקווים

27/05/2023

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

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

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

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

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

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

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

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

ואז שכחתי את הרקורסיה (או: למה אני אוהב Decorators בפייתון)

26/05/2023

קוד פייתון הבא מחשב מספר n בסידרת פיבונאצ'י בדרך מאוד לא יעילה:

def fib(n):
    if n <= 1:
        return 1

    return fib(n-1) + fib(n-2)

הבעיה איתו היא שהוא מחשב את ערכי הביניים יותר מדי פעמים. בניסיון לחשב את fib(10) הוא יחשב את הסכום fib(9)+fib(8), ואז בשביל לחשב את fib(9) הוא שוב יחשב את fib(8). אפשר להיווכח בזה בקלות אם מוסיפים שורת הדפסה בתחילת הפונקציה. הקוד יהיה:

def fib(n):
    print(f"fib({n})")
    if n <= 1:
        return 1

    return fib(n-1) + fib(n-2)

והפלט הוא:

fib(5)
fib(4)
fib(3)
fib(2)
fib(1)
fib(0)
fib(1)
fib(2)
fib(1)
fib(0)
fib(3)
fib(2)
fib(1)
fib(0)
fib(1)
8

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

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

def my_cached(f):
    cache = {}
    def cached_version_of_f(n):
        if n not in cache:
            cache[n] = f(n)
        return cache[n]

    return cached_version_of_f

הפעלת הקוד בתור דקורטור עובדת ומתקנת את הבעיה כצפוי:

def fib(n):
    print(f"fib({n})")
    if n <= 1:
        return 1

    return fib(n-1) + fib(n-2)


"""
Output:
fib(5)
fib(4)
fib(3)
fib(2)
fib(1)
fib(0)
8
"""

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

better_fib = my_cached(fib)
print(better_fib(5))

הפעם התוצאה הרבה פחות משמחת. הפלט הוא שוב כמו בהתחלה עם כל ההדפסות הכפולות:

fib(5)
fib(4)
fib(3)
fib(2)
fib(1)
fib(0)
fib(1)
fib(2)
fib(1)
fib(0)
fib(3)
fib(2)
fib(1)
fib(0)
fib(1)
8

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

fib = my_cached(fib)
print(fib(5))

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

מה קורה כאן? למה אי אפשר לשמור יחד את שתי הגירסאות של הפונקציה?


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

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

פיצ'רים שחסרים בשפה

25/05/2023

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

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

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

try:
    do_something(1)
except Exception as e:
    print("An exception occurred in code block 1:", str(e))

try:
    do_something(2)
except Exception as e:
    # Exception handling code for code block 2
    print("An exception occurred in code block 2:", str(e))

try:
    do_something(3)
except Exception as e:
    print("An exception occurred in code block 3:", str(e))

והשאלה שהתלוותה אליו - האם יש דרך לתפוס את שלושת ה Exceptions באותו try ועדיין להריץ תמיד את כל הפקודות?

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

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