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

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

אם הם רק היו עקביים...

24/12/2022

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

>>> 5 in range(0, 5)
False

ב JavaScript, עם lodash:

_.range(0, 5)
Array(5) [ 0, 1, 2, 3, 4 ]

ב Clojure:

user=> (range 0 5)
(0 1 2 3 4)

ב Rust:

for i in 0..5 {
    print!("{}", i)
}
// prints: 01234

אבל אז יש את השפות בהן האיבר האחרון הוא כן חלק מ Range. למשל Elixir:

iex(2)> Enum.member?(0..5, 5)
true

או Bash:

$ echo {0..5}
0 1 2 3 4 5

ואז יש את רובי, שמציעה שתי דרכים ליצור Range:

3.1.1 :001 > (0..5).include?(5)
 => true

3.1.1 :002 > (0...5).include?(5)
 => false

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

מאחורי כל שאלת "איך"

23/12/2022

מאחורי כל שאלת "איך" מתחבאת שאלת "למה", ובדרך כלל ה"למה" היא הרבה יותר מעניינת.

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

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

מעבר בין "חשבונות" באפליקציית Redux

22/12/2022

אחד האתגרים בעבודה עם Redux הוא ניהול אוביקט המידע, במיוחד כשהמידע שאנחנו מנהלים לא ממש מסתדר טוב עם העקרונות של רידאקס, ובמיוחד אם אנחנו מדברים על שינוי מערכת קיימת. דוגמה? בואו. ניקח מערכת Todo פשוטה שצריכה להציג רשימת משימות, מאפשרת למשתמשים למחוק משימות ולעדכן שמשימות בוצעו. אפשר ליצור כזאת בשניה וחצי ב code sandbox עם הטמפלייט redux-toolkit-typescript. הנה קישור עם קוד לסקרנים:

https://codesandbox.io/s/eager-thompson-ijimne

המשך קריאה

קוד נכון

21/12/2022

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

שתי דוגמאות מהימים האחרונים ואז שורה תחתונה-

  1. פה באתר כפתור "אחורה" בדפדפן לא עבד בחלק מהדפים. הבאג נגרם בגלל השורה:
window.history.replaceState(null, null, url.toString());

בשביל לתקן בסך הכל היה צריך להחליף את השורה ל:

window.history.replaceState(window.history.state, null, url.toString());

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

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

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

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

ואיך זה ישפיע על הפיתוח בעתיד?

20/12/2022

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

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

  2. איזה בדיקות חדשות אצטרך להריץ בכל סבב?

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

  4. איך זה יישבר?

  5. איזה פיצ'רים אחרים במערכת יושפעו?

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

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

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

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

חדש באתר: קורס TypeScript

19/12/2022

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

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

טייפסקריפט נכתבה כדי לתת למפתחים כלי עבודה טוב יותר לכתוב אפליקציות JavaScript. היא לא הולכת להפוך את JavaScript ל Java או ל C#, והיא לא הולכת להכניס שיטות עבודה "של אנשי בקאנד" לעולם הפרונט-אנד. היא בסך הכל נותנת לכם כלים טובים יותר לכתוב את ה JavaScript שאתם כבר מכירים, כדי שתוכלו לקודד מהר יותר ועם פחות הסחות דעת. אלה כמה מהפיצ'רים שלא חשבתי שאני בכלל צריך, אבל היום לא יכול בלעדיהם:

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

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

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

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

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

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

https://www.tocode.co.il/bundles/typescript

למה כל כך מסובך?

18/12/2022

ארבע סיבות מרכזיות בגללן אנחנו כותבים קוד מסובך:

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

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

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

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

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

[טייפסקריפט] דוגמה טובה לשימוש ב Function Overloading

17/12/2022

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

המשך קריאה

היום למדתי: הפקודה readarray ב bash

16/12/2022

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

ubuntu@primary:~$ read name < <(echo ynon)
ubuntu@primary:~$ echo "$name"
ynon

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

ubuntu@primary:~$ read name < <(echo "   one")
ubuntu@primary:~$ echo "$name"
one

ואם בפלט יש כמה שורות, read פשוט תזרוק את כל מה שבא אחרי השורה הראשונה:

ubuntu@primary:~$ read name < <(cowsay hello)
ubuntu@primary:~$ echo "$name"
_______

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

ubuntu@primary:~$ readarray lines < <(echo "   one")
ubuntu@primary:~$ echo "$lines"
   one

וזאת הדוגמה השלישית, שמדפיסה הפעם את כל השורות:

ubuntu@primary:~$ readarray lines < <(cowsay hello)
ubuntu@primary:~$ echo "${lines[@]}"
 _______
 < hello >
  -------
         \   ^__^
          \  (oo)\_______
             (__)\       )\/\
                 ||----w |
                 ||     ||

יותר מזה, בגלל שהפלט נשמר בתור מערך אפשר לגשת לכל שורה ממנו למשל:

echo "${lines[1]}"

ל readarray יש די הרבה אפשרויות להתאמה כולל המתג -t שמוחק את תו ירידת השורה מכל שורת קלט, המתג -n שמאפשר להגביל את מספר השורות שנקרא, המתג -s שמאפשר לדלג על השורות הראשונות.

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

ubuntu@primary:~$ cowsay hello | readarray lines

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

[ריאקט] אפקט לחישוב אסינכרוני הוא Anti Pattern

15/12/2022

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

function Twice({ x }) {
    return <p>{x} * 2 = {x * 2}</p>
}

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

// Don't Do This:
function DataFromApi({ id }) {
    const [name, setName] = useState(null);

    useEffect(() => {
        fetch(`/api/product/${id}`)
            .then(p => p.json())
            .then(p => setName(p.name));
    }, [id]);

    if (!name) {
        return <p>Loading...</p>
    }

    return <p>Product Name = {name}</p>
}

אבל זאת Anti Pattern. אלה הבעיות המרכזיות עם הגישה:

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

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

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