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

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

טיפ רשתות - מקשיבים לפורט 0

29/12/2023

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

שימו לב לקוד הבא ב node.js שמעלה שרת echo, מנסה לכתוב אליו ומוודא שקיבל בחזרה את התשובה הנכונה:

const net = require('node:net');
const assert = require('node:assert');


async function main() {
  var server = net.createServer(function(socket) {
    socket.pipe(socket);
  });

  server.listen(0, '127.0.0.1');
  await new Promise((resolve, _reject) => server.on('listening', resolve));

  const clientSocket = net.createConnection(server.address().port);
  await new Promise((resolve, _reject) => clientSocket.on('connect', resolve));

  clientSocket.on('data', (data) => {
    const dataWritten = Buffer.from('hello world\n', 'utf8');
    const dataRead = data;

    assert(dataRead.equals(dataWritten));
    clientSocket.end();
    server.close();
  });

  clientSocket.write("hello world\n");
}

main();

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

server.listen(0, '127.0.0.1');

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

איך נולד באג (או: על החשיבות של בדיקות כשמשנים קוד)

28/12/2023

נתבונן בקוד הבא ב node.js:

import net from 'node:net';

const host = "localhost";
const port = 1234;

const socket = net.createConnection(port, host, () => {
  socket.write("hello\n");
  socket.end();
});

socket.on('error', () => {
  console.log(`Error connecting to ${host}:${port}`);
});

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

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

import net from 'node:net';

const host = "localhost";
const port = Number(process.env["PORT"]) || 1234;

const socket = net.createConnection(port, host, () => {
  socket.write("hello\n");
  socket.end();
});

socket.on('error', () => {
  console.log(`Error connecting to ${host}:${port}`);
});

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

$ node demo.mjs
Error connecting to localhost:1234

$ PORT=8080 node demo.mjs
Error connecting to localhost:8080

$ PORT=abc node demo.mjs
Error connecting to localhost:1234

$ PORT=-1 node demo.mjs
node:internal/errors:496
    ErrorCaptureStackTrace(err);
    ^

RangeError [ERR_SOCKET_BAD_PORT]: Port should be >= 0 and < 65536. Received type number (-1).
    at new NodeError (node:internal/errors:405:5)
    at validatePort (node:internal/validators:409:11)
    at lookupAndConnect (node:net:1289:5)
    at Socket.connect (node:net:1246:5)
    at Object.connect (node:net:236:17)
    at file:///Users/ynonp/tmp/blog/nodebug/demo.mjs:6:20
    at ModuleJob.run (node:internal/modules/esm/module_job:217:25)
    at async ModuleLoader.import (node:internal/modules/esm/loader:308:24)
    at async loadESM (node:internal/process/esm_loader:42:7)
    at async handleMainPromise (node:internal/modules/run_main:66:12) {
  code: 'ERR_SOCKET_BAD_PORT'
}

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

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

# Test with default port
PORT=1234 node your_program.js

# Test with common ports
PORT=80 node your_program.js
PORT=443 node your_program.js

# Test with invalid port numbers
PORT=-1 node your_program.js
PORT=abc node your_program.js

# Test with a port already in use
# (Make sure another program is already using the specified port)
PORT=8080 node your_program.js

# Test with a custom port using environment variable
PORT=5678 node your_program.js

במקום לפנות זמן נסו להוסיף אנרגיה

27/12/2023

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

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

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

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

טיפ ביטויים רגולאריים - לא חוזרים על Capturing Group

26/12/2023

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

values: 10 20 30 40

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

import re
text = "values: 10 20 30 40"
m = re.search(r'values: (?:(\d+)\s*)+', text)

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

>>> m.groups()
('40',)

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

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

>>> re.findall(r'\b(\d+)\b', text)
['10', '20', '30', '40']

למה עדיין אכפת לנו מההבדל בין == ל === ?

25/12/2023

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

https://www.reddit.com/r/reactjs/comments/18muk6w/whataretheinterviewquestionsinyou_have/

רשימה השאלות על JavaScript כוללת שאלות כמו "מה ההבדל בין let ל const?" ו"מה ההבדל בין == ל ===?".

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

שני דברים קופצים לעין משאלות אלה ושאר השאלות באותו עמוד - מצד אחד החיפוש אחרי אנשים שמבינים את העקרונות, ומצד שני האיטיות בה פיצ'רים חדשים נכנסים לרשימת השאלות. אפילו השאלות הלכאורה קשות שהופיעו שם היו על ההבדל בין useMemo ל useCallback או על React.cloneElement.

פיצ'רים כמו useTransition או useSyncExternalStore לא נכנסו לרשימה ואני חושב שזה לא מקרי, וגם Server Components עדיין לא נחשבים מיינסטרים. יש עדיין פער היום בין מה שמעניין את הצוות שבונה את ריאקט לבין מה שמעניין את הקהילה שבונה מוצרים עם ריאקט. כן אנחנו אוהבים להשתמש ב next.js אבל מעדיפים שיעבוד "כמו שאנחנו מכירים". אנחנו מוכנים לחיות עם Concurrent Mode כל עוד היקף השינוי הוא קטן, ובטח שלא נחפש בראיונות אנשים שיש להם ניסיון ספציפי בפיצ'רים האלה. האתגר הכי גדול של מפתחי ריאקט היום הוא עדיין איך להשתמש נכון ב useEffect.

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

(וכמו תמיד בפוסטים מהסוג הזה - תקנו אותי אם אני טועה. התראיינתם במקום שעובד בריאקט ודרשו מכם לממש משהו עם useSynxExternalStore? אל תתביישו וספרו לי על זה בתגובות)

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

24/12/2023

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

המשך קריאה

"רק צריך להתאמץ יותר" זו מלכודת כפולה

23/12/2023

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

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

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

למה בדיקות לא מזהות באגים

22/12/2023

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

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

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

והכי גרוע - כן אנחנו כותבים בדיקות אבל עדיין יש באגים.

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

היום למדתי: לא כזה פשוט לבנות נתיב ארוך

21/12/2023

הפונקציה os.path.join של פייתון מחברת שמות של כמה נתיבים לנתיב גדול:

>>> import os
>>> os.path.join("/foo", "bar", "buz")
'/foo/bar/buz'

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

>>> import urllib.parse
>>> urllib.parse.urljoin("http://localhost", "blog", "items")
'http://localhost/blog'

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

>>> urllib.parse.urljoin('http://localhost/blog', 'items')
'http://localhost/items'

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

>>> urllib.parse.urljoin('http://localhost/blog/', 'items')
'http://localhost/blog/items'

גם JavaScript תומכת בחיבור URL-ים רק משני חלקים, אבל שם סדר הפריטים הפוך:

> new URL("items", "http://localhost/blog").toString()
'http://localhost/items'
> new URL("items", "http://localhost/blog/").toString()
'http://localhost/blog/items'

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

3.1.1 :006 > URI.join("http://localhost", "blog/", "items").to_s
 => "http://localhost/blog/items"

3.1.1 :007 > URI.join("http://localhost", "blog", "items").to_s
 => "http://localhost/items"

וגם קלוז'ר במימוש של ספריית lambdaisland/uri תואמת להתנהגות של רובי עם הלוכסנים בסוף מילים:

; "http://localhost/items"
(str (uri/join "http://localhost" "blog" "items"))

; "http://localhost/blog/items"
(str (uri/join "http://localhost" "blog/" "items"))

מסקנות -

  1. שימו לב לחתימה של URI/join בשפה שבחרתם, האם אפשר לקבל כמה פרמטרים שרוצים או רק שניים ומה הסדר שלהם.

  2. שימו לב שבחיבור נתיבים ל URL יש לסיים בלוכסן כל נתיב שאתם רוצים שיישאר בתוצאה הסופית.

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

טיפ פייתון: סידור פלט עם format

20/12/2023

כולנו מכירים את מנגנון ה format string של פייתון שמאפשר לכתוב f בתחילת מחרוזת כדי "לשתול" בתוך המחרוזת תוצאות של ביטויים בקוד פייתון לדוגמה:

print(f"10 / 3 = {10 / 3}")

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

>>> print(f"{10 / 3:.2f}")
3.33

ויש עוד למשל אפשר להפוך מספר לבסיס 16 אם נכתוב אחרי הנקודותיים את האות x:

>>> print(f"16 is base 16 is {16:x}")
16 is base 16 is 10

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

>>> format(16, 'x')
'10'
>>> format(10 / 3, '.2f')
'3.33'
>>> format('hello', '^20s')
'       hello        '

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