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

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

איך לא לאכול את הלב

05/09/2023

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

״ידעתי שלא הייתי צריך להסכים לשלב את ה Elastic Search, אני לא מאמין איך איבדנו חצי שנה של פיתוח על בעיות הביצועים שלו״

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

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

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

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

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

חשבתי שב Outsource אוכל לחסוך זמן, אבל בסוף התאכזבתי כי לא מצאתי את המפתח הנכון.

חשבתי שזה יהיה רעיון מדליק - אבל בסוף זה לא היה.

באיזה פורטים הוא משתמש?

04/09/2023

קחו קובץ docker-compose.yml גדול, עם הרבה סרביסים ונסו לענות על השאלה "באיזה פורטים הוא משתמש?". כלומר לא עבור סרביס ספציפי (את זה אני יכול לחפש בקובץ אין בעיה), אלא לכל הסרביסים יחד. חיפוש בעורך הטקסט? אין צורך - אני מכיר yq:

$ yq '.services.[].ports|select(. != null)' < docker-compose.yml| tr -d \" | sed 's/- //'  | cut -d: -f1

הפלט הוא רשימה של כל הפורטים ב host שכל הסרביסים בקובץ docker-compose.yml יתפסו.

בואו נשתמש ב Container Queries כדי לקבוע גודל של טקסט לפי רוחב המיכל

03/09/2023

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

.text {
  font-size: 300%;
}

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

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

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

עד לפני מספר חודשים הדרך לעשות את ההתאמות האלה היתה להשתמש ב JavaScript - לתפוס אירועי שינוי גודל על אלמנט המיכל ולשנות את המאפיינים בהתאמה. בעזרת יכולת שנקראת Container Query אנחנו יכולים לקבוע מאפיינים שונים לפריטים לפי גודל המיכל שלהם.

התחביר מאוד דומה ל Media Query ומספק שני פיצ'רים שימושיים:

  1. יחידות מידה חדשות, בדומה ל vw ו vh, רק שהפעם יהיו יחסיות לגודל הקונטיינר.

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

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

container-type: 'size';

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

.text {
  background: blue;
  // cqw -> container width
  font-size: 4cqw;
  padding: 10px;
}

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

@container (min-width: 700px) {
  .text {
    background: orange;
  }
}

דוגמה? בטח. נסו את הקודסנדבוקס הבא: https://codesandbox.io/s/cool-phoebe-kkmpr6

או בהטמעה כאן:

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

שימו לב: פלוס שווה ב JavaScript לא באמת מוסיף

02/09/2023

הקוד הבא מבלבל:

let x = "hello, "
x += "world"
console.log(x);

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

קל להיווכח בזה באמצעות הוספת משתנה:

> x = "hello, "
'hello, '
> y = x
'hello, '
> x += "world"
'hello, world'
> x
'hello, world'
> y
'hello, '

אם ה += היה משנה את המחרוזת אז היינו רואים את השינוי גם ב y, בדיוק כמו שקורה בעבודה על מערכים:

> x = [1, 2, 3]
[ 1, 2, 3 ]
> y = x
[ 1, 2, 3 ]
> x.push(4)
4
> x
[ 1, 2, 3, 4 ]
> y
[ 1, 2, 3, 4 ]

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

דוגמה? תמיד. הקוד הבא מייצר כפתור שבלחיצה עליו יקפיץ הודעת alert:

const main  = document.querySelector('main');
main.innerHTML += '<button>button 1</button>';
main.querySelector('button').addEventListener('click', () => {
  alert('1');
})

ועכשיו אני לוקח את אותו קוד ומוסיף לו שורה:

const main  = document.querySelector('main');
main.innerHTML += '<button>button 1</button>';
main.querySelector('button').addEventListener('click', () => {
  alert('1');
});

main.innerHTML += '<p>Click for magic</p>';

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

היום למדתי: throw ב JavaScript אינו Expression

01/09/2023

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

const id = searchParams.id || throw new Error("id is required");

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

open FILE, "filename.txt" or die "Cannot open file: $!\n";

או:

chdir('/etc') or die "Can't change directory";

אז למה בעצם לא היינו צריכים את זה ב JavaScript? כמה השערות-

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

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

const { id, page, limit } = searchParams;
if (!id) {
    throw new Error("id is required");
}

מה אתכם? יצא לכם להתגעגע למבנה של || throw ב JavaScript? אם כן ספרו מתי, ואם לא ספרו מה עשיתם במקום.

החלום ושברו

31/08/2023

כשחלום פוגש את המציאות הוא אף פעם לא יפגוש מציאות שמתאימה לו במאה אחוז. אם החלום היה יכול לקרות בתור מציאות הוא כבר היה קורה ולא היה חלום.

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

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

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

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

טיפים לעבודה יעילה עם bfcache

30/08/2023

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

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

המשך קריאה

הטיפוס המינימלי

29/08/2023

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

def longer_than(n: int, items: list[str]) -> list[str]:
    return [I for I in items if len(i) > n]

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

print(longer_than(3, ((1, 2, 3), (2, 3, 4, 5, 6), range(10))))

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

def longer_than(n: int, items: Iterable[Sized]) -> list[Sized]:
    return [I for I in items if len(i) > n]


print(longer_than(3, ((1, 2, 3), (2, 3, 4, 5, 6), range(10))))

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

long_items = longer_than(3, ((1, 2, 3), (2, 3, 4, 5, 6), range(10)))
print(long_items[0][0])

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

הפיתרון כמו שאני מקווה שאתם כבר יודעים הוא TypeVar שמוכן לקבל כל טיפוס יותר ספציפי מ Sized, כלומר:

T = TypeVar('T', bound=Sized)  
def longer_than(n: int, items: Iterable[T]) -> list[T]:
    return [I for I in items if len(i) > n]

long_items1 = longer_than(3, ((1, 2, 3), (2, 3, 4, 5, 6), range(10)))
print(long_items1[0][0])  

long_items2 = longer_than(3, ['abc', 'abcdefg', 'as'])
print(long_items2[0].capitalize())

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

בעידן של ChatGPT, הכל זה התלבטויות בין אפשרויות עובדות

28/08/2023

״איך את בונה תיבה עם השלמה אוטומטית שתהיה נגישה?״

״איך אתה בונה Checkbox שישתמש בתמונה מותאמת אישית וגם יאפשר ניווט מקלדת?״

״איך תבני גלריית תמונות ב HTML/CSS אם כל התמונות בגדלים שונים?״

״איך תציג על המסך תמונה עם אפקט Scale כשעוברים עליה עם העכבר?״

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

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