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

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

פיתרון Advent Of Code 2023 יום 12 ב Scala

17/02/2024

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

המשך קריאה

יותר מדי אינפורמציה

16/02/2024

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

"שמע השבוע התחלתי ללמוד Tensor Flow כי בכל מקום צריכים את זה"

"סיימתי קורס פייתון של 40 שעות וידאו ביודמי - היה מעולה ולמדתי המון"

"תגיד מה דעתך על AWS? התחלתי ללמוד להסמכה של CLF-C01 כי הבנתי שחייבים את זה בשביל להתקבל לעבודה כ MLOps"

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

אבל מעגל הקסמים הזה לא מסתיים.

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

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

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

זה לא מספר השורות

15/02/2024

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

def to_list_of_digits(s: str) -> list[int]:
    result = []
    for ch in s:
        if ch.isdigit():
            result.append(int(ch))
    return result

וזה עובד! אבל אז אנחנו מגלים שבעצם בפייתון יש מנגנון שנקרא List Comprehension ושאנחנו יכולים לכתוב את הפונקציה בצורה הרבה יותר קצרה:

def to_list_of_digits(s: str) -> list[int]:
    return [int(ch) for ch in s if ch.isdigit()]

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

התשובה מורכבת אבל כדאי להשאיר בראש כמה נקודות-

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

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

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

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

מתי בכל זאת לתרום לפרויקט קוד פתוח?

14/02/2024

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

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

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

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

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

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

טיפ פייטסט: איך ועל איזה בדיקות לדלג?

13/02/2024

אחד הפיצ'רים החמודים של פייטסט הוא היכולת "לסמן" בדיקות בכל דרך שתבחרו. יוצרים קובץ בשם pytest.ini עם תוכן שנראה בערך ככה:

[pytest]
markers =
    integration: integration test
    slow: slow test
    version: tests to run before deploying a new version

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

@pytest.mark.integration
def test_website():
    pass

ולהריץ את כל הבדיקות שמסומנות בקטגוריה integration:

$ pytest -m integration

או להריץ את כל הדברים שלא מהקטגוריה עם:

$ pytest -m "not integration"

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

אבל האתגר היותר משמעותי הוא לא איך להשתמש בפיצ'ר אלא מתי להשתמש בו - כלומר על איזה בדיקות כדאי לדלג ואיזה קטגוריות להגדיר, מתי להוסיף xfail ומתי skip ומתי בכלל עדיף למחוק את הבדיקה.

ננסה לענות על זה בכמה כללי אצבע-

  1. רוב הפיצ'רים של בדיקות הם יותר טובים כשלא משתמשים בהם. זה נכון לגבי mock-ים, לגבי before ו after וכן גם לגבי דילוגים. אם אתם יכולים בלי זה עדיף.

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

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

  4. בדף התיעוד יש דוגמה ל skipif שמדלגת על בדיקה לפי מערכת הפעלה. אישית כשאני כותב בדיקה שצריכה לרוץ רק על מערכת הפעלה מסוימת אני אעדיף לא לראות אותה ב skip או ב xfail כי המשמעות של סימונים אלה היא בדרך כלל שיש איזה בעיה בבדיקה. במקום זה הייתי בקוד הבדיקה מוסיף את הבדיקה ומסמן "הצלחה" אם זאת לא מערכת ההפעלה המתאימה.

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

זה צריך לקחת שבועיים

12/02/2024

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

  1. לא בדקתי, אבל נראה לי שזה מה שהיה לוקח לי.

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

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

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

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

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

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

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

פיתרון Advent Of Code 2023 יום 11 חלק 1 בסקאלה

11/02/2024

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

המשך קריאה

דברים קטנים

10/02/2024

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

״למה הקונטיינר שלי לא נדלק?!״

״למה אין עדיין רווחים מהמערכת?!״

״למה הדף נטען כל כך לאט?!״

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

  1. ללמוד את עולם התוכן ברמה מספיק טובה כדי להתמודד עם האתגר הזה.

    • או - לשלם למישהו שלמד כבר את עולם התוכן כדי שיעזור לנו לפתור את האתגר.

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

סוגריים

09/02/2024

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

  @main
  def parens(): Unit =
    val result = if (Random.nextInt() > 0) {
      Try { throw new Exception("20") }
    } else {
      Try { throw new Exception("30") }
    }.recover { err => 30 }

    println(result)

והשאלה מה הערך של result?

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

  @main
  def parens(): Unit =
    val result = if (Random.nextInt() > 0) {
      Try { throw new Exception("20") }
    } else {
      Try { throw new Exception("30") }
    }

ו result יהיה שווה לאוביקט Failure שה Exception שלו תלויה בתוצאה של החישוב האקראי. פקודת recover בסקאלה שמופעלת על אוביקט Try משנה אותו מ Failure ל Success עם הערך שמופיע ב recover. הדוגמה הבאה לכן גם צפויה:

  @main
  def parens(): Unit =
    val result = if (Random.nextInt() > 0) {
      Try { throw new Exception("20") }
    } else {
      Try { throw new Exception("30") }
    }

    println(result.recover { err => 30 })

בגלל שלא משנה מה הוגרל result החזיק בכל מקרה Failure, אז הפעלת recover תשנה את הערך ל Success של 30 וזה מה שיודפס:

Success(30)

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

    val result = if (Random.nextInt() > 0)
                    Try { throw new Exception("20") }
                 else Try { throw new Exception("30")

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

val result = if (Random.nextInt() > 0)
                Try { throw new Exception("20") }
                else
                Try { throw new Exception("30") }.recover { _ => 30 }

עכשיו זה ברור - ה recover השפיע רק על בלוק ה else ולא על כל ה if. הקוד החזיר Success כשהמספר האקראי היה 0 או שלילי, ובמספרים חיוביים החזיר Failure. כשמבינים איך זה עובד קל גם לתקן:

  @main
  def parens(): Unit =
    val result = (if (Random.nextInt() > 0) {
      Try { throw new Exception("20") }
    } else {
      Try { throw new Exception("30") }
    }).recover { _ => 30 }

    println(result.recover { err => 30 })

ואז גיליתי

08/02/2024

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

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

פונקציה שמטפלת בלחיצה על כפתור מקבלת את הפרמטרים הבאים: flags, game, peer, msg_id, data, password, ולכן הדבר שנראה לי הכי הגיוני כשניגשתי לממש את זה פעם ראשונה היה לשים את המידע החשוב בשדה data של הכפתור, ואז לקבל את המידע הזה בטיפול בלחיצה. בדוגמה של הלייק נשים את ה URL של הפוסט בתור data, נקבל אותו בפונקציה שתטפל בלחיצה כל הכפתור והכל טוב.

(ואז גיליתי)

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

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

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

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

  3. אפשר לשים לב לפרמטר נוסף msg_id ברשימת הפרמטרים של טיפול בלחיצה. בעזרת מזהה ההודעה אני יכול להגיע להודעה שכללה את הכפתור ולהסתכל שם מה היה ה URL של הפוסט.

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

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