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

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

חדש באתר: מדריך sed

13/05/2023

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

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

כל ההקדמה הזאת באה כדי לספר שני דברים - הראשון, שלא מזמן פנתה אליי תלמידה בבקשה ללמוד יותר על פקודת sed, ובזכותה העליתי עכשיו מדריך וידאו חדש של כחצי שעה על הפקודה. המדריך כולל גם הסבר תיאורטי על sed וגם אינספור דוגמאות לדברים שעבורם אני משתמש ב sed בעבודה היום יומית. אתם יכולים למצוא את המדריך בחינם בקישור: https://www.tocode.co.il/boosters/12

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

שמות משתנים בעלי משמעות

12/05/2023

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

const days = 

קופיילוט משלים את השורה ל:

const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];

ההתחלה הזו:

const baseUrl = 

הופכת ל:

const baseUrl = import.meta.env.BASE_URL;

וההתחלה הזו:

const ticTacToeGameBoard = 

קיבלה את ההשלמה ל:

const ticTacToeGameBoard = [
  ['', '', ''],
  ['', '', ''],
  ['', '', ''],
]

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

function SignupForm() {

קיבלה השלמה ל:

function SignupForm() {
  return (
    <form>
      <label htmlFor="email">Email</label>
      <input type="email" id="email" />
      <label htmlFor="password">Password</label>
      <input type="password" id="password" />
      <button type="submit">Sign up</button>
    </form>
  )
}

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

היום למדתי: את המתג `-l` של gcc צריך לכתוב בסוף

11/05/2023

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

gcc -l m

אז היום גיליתי שהצלחתי להעביר חיים שלמים בלי לשים לב שיש חשיבות למקום של -l בתוך שורת הפקודה של הקומפילציה, או מתוך התיעוד:

It makes a difference where in the command you write this option; the linker searches and processes libraries and object files in the order they are specified. Thus, foo.o -lz bar.o searches library z after file foo.o but before bar.o. If bar.o refers to functions in z, those functions may not be loaded.

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

#include <stdio.h>
#include <curl/curl.h>
int main(void)
{
  CURL *curl;
  CURLcode res;
  curl = curl_easy_init();
  if(curl) {
    curl_easy_setopt(curl, CURLOPT_URL, "http://example.com/");
    /* Perform the request, res will get the return code */
    res = curl_easy_perform(curl);
    /* Check for errors */
    if(res != CURLE_OK)
      fprintf(stderr, "curl_easy_perform() failed: %s\n",
              curl_easy_strerror(res));
    /* always cleanup */
    curl_easy_cleanup(curl);
  }
  return 0;
}

ושם הקובץ getter.c, אז ניסיון לקמפל את הקובץ עם:

$ gcc -l curl getter.c

מחזיר:

/usr/bin/ld: /tmp/ccToWOcP.o: in function `main':
getter.c:(.text+0x84): undefined reference to `curl_easy_init'
/usr/bin/ld: getter.c:(.text+0xb0): undefined reference to `curl_easy_setopt'
/usr/bin/ld: getter.c:(.text+0xb8): undefined reference to `curl_easy_perform'
/usr/bin/ld: getter.c:(.text+0xdc): undefined reference to `curl_easy_strerror'
/usr/bin/ld: getter.c:(.text+0xf8): undefined reference to `curl_easy_cleanup'
collect2: error: ld returned 1 exit status

והדרך הנכונה לקמפל את הקובץ היא:

$ gcc getter.c -l curl

שילוב asyncio עם Threads בפייתון

10/05/2023

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

נדמיין שיש לנו קוד שמשתמש ב AWS כדי לתרגם מילים:

import boto3

client = boto3.client('translate')

def spanish_to_english(text):
    response = client.translate_text(
        Text=text,
        SourceLanguageCode='es',
        TargetLanguageCode='en',
    )

    return response["TranslatedText"]

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

def blocking_io():
    print(f"start blocking_io at {time.strftime('%X')}")
    # Note that time.sleep() can be replaced with any blocking
    # IO-bound operation, such as file operations.
    time.sleep(1)
    print(f"blocking_io complete at {time.strftime('%X')}")

async def main():
    print(f"started main at {time.strftime('%X')}")

    await asyncio.gather(
        asyncio.to_thread(blocking_io),
        asyncio.sleep(1))

    print(f"finished main at {time.strftime('%X')}")


asyncio.run(main())

קריאה רגילה ל blocking_io היתה חוסמת את ה Event Loop וגורמת להמתנה כוללת של שתי שניות. בזכות השימוש ב to_thread, הפונקציה נקראת בתהליכון נפרד וה sleep מתבצע במקביל ל asyncio.sleep, כך שסך הכל התוכנית ממתינה שניה אחת לשתי הפעולות.

הפונקציה נכנסה לשפה בפייתון 3.9 ואתם יכולים למצוא עליה מידע נוסף יחד עם כל הפרטים הקטנים בדף התיעוד: https://docs.python.org/3/library/asyncio-task.html#running-in-threads

היום למדתי: שיפור ביצועים עם Metadata ב Clojure

09/05/2023

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

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

:global-vars {*warn-on-reflection* true}

ובתוך ה repl אפשר להפעיל פשוט:

(set! *warn-on-reflection* true)

כדי להדליק את האזהרות.

עכשיו עם המשתנה דלוק אני יכול לראות שבקוד הבא קלוז'ר מבין בקלות מה הקלאס של str:

=> (set! *warn-on-reflection* true)
=> (let [s "hello there"] (.charAt s 3))
\l

אבל בקוד הזה אין לו מושג ולכן הוא צריך להשתמש ב Reflection כדי לזהות בזמן ריצה את הקלאס:

=> (set! *warn-on-reflection* true)
=> (let [s (apply str "hello")] (.charAt s 3)
Reflection warning, NO_SOURCE_PATH:1:30 - call to method charAt can't be resolved (target class is unknown).
\l

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

אנחנו מדביקים Metadata לערך עם המקרו המיוחד ^, ומושכים את ה metadata עם פונקציית meta. הקוד הבא מגדיר את המשתנה x להחזיק רשימה עם המספרים 1,2,3 ומשייך אליו את ה Metadata שנקרא :yay:

user=> (def x ^:yay [1 2 3])
#'user/x
user=> x
[1 2 3]
user=> (meta x)
{:yay true}

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

user=> (set! *warn-on-reflection* true)
user=> (let [s ^String (apply str "hello")] (.charAt s 3))
\l

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

user=> (let [s ^Integer (apply str "hello")] (.charAt s 3))
Reflection warning, NO_SOURCE_PATH:1:39 - call to method charAt on java.lang.Integer can't be resolved (no such method).
\l

יש מחיר להבנה

09/05/2023

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

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

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

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

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

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

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

הפחד להתאמץ מדי

08/05/2023

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

  1. הנה תרגיל קשה אבל אני יודע בגדול איך פותרים אותו,

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

  3. בגלל שאני יודע מה הפיתרון, לפתור את האתגר לא ייתן לי יותר מדי,

  4. חבל להשקיע מאמץ סתם, עדיף לעשות דברים יותר מועילים.

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

  1. הנה תרגיל קשה אבל אני יודע בגדול איך פותרים אותו,

  2. אני יושב את השלוש שעות שחשבתי שזה ייקח, מגלה שהכיוון שלי לא היה נכון, ממשיך לשבור את הראש ופותר את האתגר אחרי 7 או 17 שעות.

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

כל עבודה על פאזל היא הזדמנות. הזדמנות לשבור את הראש. הזדמנות לתת כל מה שיש לך ויותר מזה בשביל לפתור אותו. לא בשביל הפיתרון (כי תמיד אפשר לשאול את Chat GPT), אלא כי המאמץ הוא ההצדקה של עצמו. להתאמץ מדי זו הדרך היחידה קדימה.

תיקון שימוש מסורבל בפונקציה sorted בפייתון

07/05/2023

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

import wikipedia

def k(i):
    """ returns the number of words the value i has in wikipedia """
    return len(wikipedia.summary(i))


words = ["wikipedia", "Python (programming language)", "Ruby (programming language)", "Rust (programming language)"]

print([
    w[0] for w in
    sorted([(w, k(w)) for w in words], key=lambda i: i[1], reverse=True)])

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

המשך קריאה

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

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