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

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

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

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. ברור לי איך זה קרה - המנגנון התחיל עם קריאה אחת, ואז מישהו היה צריך קריאה נוספת ולכן העתיק במקום לכתוב פונקציה (כי זה יותר קל), והשלישי כבר אפילו לא התלבט.

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

השאלה הלא נכונה

24/05/2023

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

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

  1. למה הילדים שלי כל כך בלתי נסבלים?

  2. למה אף פעם אין לי כסף?

  3. למה החיים של כולם יותר מוצלחים משלי?

  4. למה לא עשיתי תואר כשהיתה לי הזדמנות?

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

  1. למה אני אף פעם לא מצליח ללמוד טכנולוגיות חדשות בזמן?

  2. מה הדרך הכי מהירה לגרום לקוד הזה לעבוד?

  3. איך להגיע ל 100% כיסוי בדיקות?

  4. אנגולר או ריאקט?

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

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

  2. מה הדרך הכי מהירה להבין את הקוד?

  3. איזה סט בדיקות אני צריך כדי להיות רגוע לגבי המערכת שאני בונה?

  4. איך לכתוב קוד שיהיה קל לשכתב אותו כשיגיע הרגע, כי Refactor זה משהו שקורה כל הזמן.

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

טיפ neo4j - בחירת צומת באקראי

23/05/2023

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

בשביל הדוגמה אני לוקח את בסיס הנתונים Movies מתוך התבנית באתר https://sandbox.neo4j.com. אתר זה מאפשר להקים בסיסי נתונים של neo4j בקלות למשחקים אונליין. בסיס הנתונים של הסרטים כולל מידע על סרטים והאנשים שלקחו בהם חלק, למשל השאילתה:

MATCH (m:Movie) WHERE m.title = 'The Polar Express'
RETURN m.released

תחזיר לי באיזה שנה יצא הסרט The Polar Express, והשאילתה:

MATCH (m:Movie) WHERE m.title = 'The Polar Express'
MATCH (p:Person)-[:ACTED_IN]-(m)
RETURN p.name

תחזיר את שמות השחקנים מהסרט.

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

MATCH (m:Movie)
RETURN m.title
ORDER BY rand()
LIMIT 1

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

MATCH (p:Person{name: "Tom Hanks"})
MATCH (p)-[:ACTED_IN]-(m:Movie)
RETURN m.title

אבל טריק ה rand כבר לא ייתן לי סרט אקראי:

// always returns Apollo 13

MATCH (p:Person{name: "Tom Hanks"})
MATCH (p)-[:ACTED_IN]-(m:Movie)
RETURN m.title
ORDER BY rand()
LIMIT 1

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

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

MATCH (p:Person{name: "Tom Hanks"})
MATCH (p)-[:ACTED_IN]-(m:Movie)
WITH collect(m) as movies
RETURN apoc.coll.randomItem(movies)

סך הכל אפשר למצוא את כל הפונקציות המובנות ב neo4j בתיעוד בקישור: https://neo4j.com/labs/apoc/4.2/overview/, ולקוות לעתיד וורוד ונטול SQL.

ניסוי - תוצאה - ניתוח - שיפור

22/05/2023

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

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

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

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

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

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

מחזיק את זה לא נכון

21/05/2023

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

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

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

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

זה שיודע הכל

20/05/2023

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

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

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

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

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

איך להעביר מידע בין שלבים ב Github Action

19/05/2023

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

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

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

KEY=value

את שם הקובץ הם לא מספרים לכם, אבל מעבירים ל yml בתור משתנה סביבה שנקרא GITHUB_OUTPUT. בשביל לקרוא מהקובץ משתמשים במשתנה המיוחד ${{steps.STEP_ID.outputs.KEY}}. דוגמה? ברור בשביל זה באנו-

# This is a basic workflow to help you get started with Actions

name: CI

on:
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Run a one-line script
        id: start
        run: |
          echo ONE=1 >> $GITHUB_OUTPUT
          echo TWO=1 >> $GITHUB_OUTPUT

      - name: Run a multi-line script
        run: |
          echo one is ${{steps.start.outputs.ONE}}
          echo two is ${{steps.start.outputs.TWO}}

השלב השני ב yml כותב לסוף הקובץ GITHUB_OUTPUT שורות בפורמט הדרוש, והשלב השלישי קורא את המידע ומדפיס אותו עם echo למסך.

טיפ קלוז'ר: שימוש באוסף כפונקציה

18/05/2023

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

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

(def items #{1 2 3})

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

(items 2) => 2

אותה התנהגות קיימת גם למערכים. אני מגדיר מערך עם סוגריים מרובעים:

(def items ["a" "b" "c" "d"])

וניגש לאינדקס מסוים באמצעות "הפעלת" המערך על האינדקס:

(items 2) => "c"

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

(def items {:foo 10 :bar 20})

וניגש לאיבר עם:

(items :foo) => 10

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

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

שימוש נאיבי ב remove כדי למחוק את המספרים 2, 3 ו-5 מתוך רצף עשוי להיות:

(remove #(or
          (= % 2)
          (= % 3)
          (= % 5)) (range 10)) => (0 1 4 6 7 8 9)

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

(remove #{2 3 5} (range 10))

ולקבל בדיוק את אותה תוצאה בכתיב ברור וקצר יותר.