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

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

שבע שנים ועדיין לא יודע גיט

06/05/2023

משתמש רדיט כתב השבוע-

Hello everyone,

I've been using Git for the last 7 years, but I still don't fully understand how it works. I'm hoping someone can help me clarify my understanding of how cloning, feature branches, and merging work in Git.

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

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

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

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

הקוד הכי גרוע שכתבתי

05/05/2023

היום הוא שוב תקף אותי, חוזר כאילו משום מקום כשכבר חשבתי שקברתי אותו לנצח - הקוד הכי גרוע שכתבתי. ככה הוא נראה בתוך Ruby Class שיורש מ ActiveRecord::Base:

def is_valid?
  return false if remaining && (remaining <= 0)

  now = Time.now.to_date
  now >= start_date && now <= end_date
end

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

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

class Invoice < ApplicationRecord
  validate :expiration_date_cannot_be_in_the_past,
    :discount_cannot_be_greater_than_total_value

  def expiration_date_cannot_be_in_the_past
    if expiration_date.present? && expiration_date < Date.today
      errors.add(:expiration_date, "can't be in the past")
    end
  end

  def discount_cannot_be_greater_than_total_value
    if discount > total_value
      errors.add(:discount, "can't be greater than total value")
    end
  end
end

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

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

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

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

04/05/2023

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

ניקח לדוגמה את הרשימות:

l1 = ["one", "two", ""]
l2 = ["one", "", "three"]
l3 = ["one", "two", "three"]

עכשיו נשאל - באיזה רשימה כל התאים הם באורך 1 לפחות?

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

def check_list(l: [str]):
    return all((len(i) > 0 for i in l))

שווה לשים לב:

  1. השימוש בסוגריים עגולים ולא במרובעים אומר שפייתון יבנה Generator ולא רשימה מלאה. בצורה כזאת all יכולה לעצור ב False הראשון במקום לחשב מראש את ערך התנאי על כל התאים ברשימה.

  2. אפשר ורצוי לקרוא עוד על all ועל החברה שלה any בדף התיעוד על פונקציות מובנות בפייתון.

מתי למזג ל main?

03/05/2023

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

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

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

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

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

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

מכירים גישות נוספות? איך עובדים אצלכם בצוות? ואיזה גישה אתם מעדיפים?

החזרת מספר בטווח ב TypeScript

02/05/2023

נתבונן בפונקציה הבאה בטייפסקריפט:

function createRandomNumber(): number {
    return Math.floor(Math.random() * 10);
}

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

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

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

type Range10 = 0|1|2|3|4|5|6|7|8|9;
function createRandomNumber() {
    return Math.floor(Math.random() * 10) as Range10;
}

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

הפיתרון הגנרי משתמש ברקורסיה ונראה כך:

export type Range<N extends number,R extends number[]=[]> = 
    R['length'] extends N ?  R[number] : Range<N, [...R, R['length']]>;


function createRandomNumber() {
    return Math.floor(Math.random() * 10) as Range<10>;
}

וההסבר-

  1. הרקורסיה בונה את המערך R, כך שכל איטרציה היא מוסיפה ל R מספר נוסף.

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

  3. בסוף הרקורסיה מחזירים איחוד של כל המספרים השמורים ב R.

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

איך לבלבל את החברים עם קובץ usercustomize.py

01/05/2023

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

(וכן זה עובד אפילו עם ה REPL9).

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

python -c "import site; print(site.getsitepackages())"

אצלי היא הדפיסה:

['/Users/ynonp/.pyenv/versions/3.11.1/lib/python3.11/site-packages']

אני יוצר קובץ חדש בתוך התיקיה בשם usercustomize.py, ובתוכו כותב את התוכן הבא:

print("I will NOT go away. I do NOT wish to go!")

בואו ננסה את זה. אני מפעיל REPL ומקבל את הפלט:

I will NOT go away. I do NOT wish to go!
Python 3.11.1 (main, Apr  6 2023, 09:09:27) [Clang 14.0.0 (clang-1400.0.29.202)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

אני מפעיל שרת HTTP ומקבל:

$ python -m http.server 8000

I will NOT go away. I do NOT wish to go!
Serving HTTP on :: port 8000 (http://[::]:8000/) ...

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

אז למה זה טוב? (חוץ מלבלבל את החברים). הנה כמה רעיונות:

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

  2. אפשר להוסיף עוד ספריות ל PYTHONPATH כדי לטעון מהן מודולים.

  3. אפשר לתעד כל הפעלה של פייתון או למנוע הפעלות מסוימות (לדוגמה אפשר להוסיף sys.exit בתוך הקובץ אם לא רוצים לאפשר הפעלה מסוימת).

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

יש לכם עוד רעיונות טובים לשימוש ב usercustomize.py? נתקלתם ב Use Cases מעניינים איתו? ספרו לי בתגובות.

החיים באי וודאות

30/04/2023

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

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

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

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

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

אבל היכולת לחיות עם אי וודאות היא הכרחית אם החלום שלנו הוא לפתור בעיות מעניינות (כאלה ש Chat GPT לא יודע לפתור).

הנה כמה רעיונות איך לאמן את שריר החיים עם אי הוודאות, בהתחלה על דברים לא חשובים ולאט לאט גם על דברים יותר חשובים-

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

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

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

  4. נסו לבנות פרויקט צד ולהביא אותו לרמה של מוצר שאתם יכולים למכור. לא משנה מה הפרויקט.

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

השוואה סובייקטיבית לגמרי בין neo4j ל datomic

29/04/2023

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

המשך קריאה

סיבה ותוצאה

28/04/2023

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

ורציתי לשאול אותה - למה את עושה את זה לעצמך? את הרי כבר כל כך ספורטיבית. בשביל מה את צריכה לצאת שוב לרוץ? למה לסבול ככה?

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

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

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

איפה להשקיע את זמן הלימוד שלי?

27/04/2023

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

שני הצירים מחלקים את העולם ל-4 ריבועים:

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

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

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

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

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

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