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

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

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

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.

[טייפסקריפט] ואז Parameters הציל את היום

14/12/2022

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

const fetcher = url => fetch(url).then(r => r.json())

אבל איך מתרגמים את זה לטייפסקריפט?

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

const { data, error } = useSWR({ url: '...', method: 'POST' });

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

מה שאנחנו באמת רוצים כאן זה להגדיר את fetcher שיקבל בדיוק את הפרמטרים ש fetch מקבלת. ואיך כותבים את זה? תשמחו לשמוע שבטייפסקריפט יש Utility Type שנקרא Parameters. זה עובד ככה:

const fetcher = (...args: Parameters<typeof fetch>) => fetch(...args).then(res => res.json())

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

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

13/12/2022

ניקח מערך של פריטים קבועים, למשל מערך של צבעים:

const colors = ['red', 'blue', 'green', 'white'];

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

function getColorName(index: number) {
    return colors[index];
}

וזה עובד כמובן אבל אנחנו לא נהנים מבדיקת הטיפוסים - אני יכול להפעיל את הפונקציה על אינדקסים חוקיים או לא חוקיים והכל יעבוד:

// compiles OK
getColorName(1);

// also compiles OK - but 129 is not a valid index
getColorName(129);

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

function getColorName(index: 0|1|2|3) {
    return colors[index];
}

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

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

type StringToNumber<T extends string, A extends any[] = []> =
  T extends keyof [0, ...A] ? A['length'] : StringToNumber<T, [0, ...A]>

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

ומשתמשים בה כדי לייצר טיפוסים באופן הבא:

// Two == 2
type Two = StringToNumber<'2'>;

// Three == 3
type Three = StringToNumber<'3'>;

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

type Error = StringToNumber<'hello'>;

עם השגיאה המופלאה:

Type instantiation is excessively deep and possibly infinite.(2589)

וזה מהמם כי אנחנו יכולים "לתפוס" את השגיאה ולהשתמש בה כדי לזרוק מפתחות מיותרים בעזרת Mapped Types. הרעיון הוא לקחת את כל המפתחות ב items, שזה כל האינדקסים (בתור מחרוזות) אבל גם המאפיין length וכל שאר הפונקציות של המערך, ולסנן מהם רק את אלה שאפשר להמיר למספר, ועל הדרך גם להמיר אותם למספרים. בסוף נפעיל keyof על התוצאה ונקבל את איחוד המספרים שחיפשנו. זה הקוד:

type ValidIndex = keyof {
    [i in keyof typeof items as (i extends string ? StringToNumber<i> : never)]: 2;
}

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

// Compiles OK - as 2 is a valid index in the array
const valid: ValidIndex = 2;

// Compilation error - 10 is not a valid index
const invalid: ValidIndex = 10;

רגע, למה זה עובד?

12/12/2022

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

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

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

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

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

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

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

פיתרון Advent Of Code 2022 יום 6 ביוניקס

11/12/2022

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

mjqjpqmgbljsphdztnvjfqwrcgsmlb

ואנחנו רואים שבקריאה משמאל לימין הפעם הראשונה שמופיעים 4 תווים שונים היא התווים jqpm ולכן האינדקס של התו שאחריהם (האות g) הוא 7.

בשביל למצוא אותו אפשר לתת ל bash לעבוד:

  1. נרוץ בלולאה עם משתנה לולאה i שמתחיל מ-1.
  2. לכל ערך של i נחתוך מהמחרוזת את 4 התווים החל מהמקום ה i בעזרת cut.
  3. נשלח את egrep לחפש תו כפול במחרוזת. אם הוא לא מצא אפשר לצאת מהלולאה ולהדפיס את i+4-1

בהנחה שהקלט נשמר בקובץ input.txt תוכנית ה bash המתאימה היא:

#!/bin/bash

i=1
while cat input.txt | cut -c $i- | cut -c -4 | egrep '(\w).*\1' >& /dev/null
do
    (( i++ ))
done

echo $(( i + 4 - 1))

או בשורה אחת:

i=1; while cat input.txt| cut -c $i- | cut -c -4 | egrep  '(\w).*\1' >& /dev/null ; do; (( i ++ )); done; echo $(( i + 4 - 1))

שלוש נקודות לקחת מהתוכנית:

  1. הרבה פעמים נוח לפצל תנאי לכמה תנאים ב Pipeline. במקרה שלנו בשביל לחתוך 4 תווים החל ממקום i אפשר לשבור את השיניים בשביל ליצור פקודת cut אחת, אבל יותר קל לפצל את זה לשתי פקודות cut אחת אחרי השניה, אחת מתחילה מהמקום ה i והשניה חותכת 4 תווים.

  2. מאוד נוח שב bash פקודת while מבצעת פקודה שהיא מקבלת שוב ושוב כל עוד הפקודה מצליחה.

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

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