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

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

שתי דרכים לבנות מערכת (או: עניין של העדפה)

07/12/2022

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

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

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

טיפ ריסלקט - שימו לב כשאתם מפעילים filter או map בתוך Selector

06/12/2022

ספריית Reselect עוזרת לנו לבנות פונקציות שמושכות מידע מ Redux Store. ריסלקט מגיעה כחלק מספריית Redux Toolkit ובשביל להשתמש בה בפרויקט אנחנו צריכים רק לייבא את הפונקציה המרכזית שלה createSelector:

import { createSelector } from "@reduxjs/toolkit";

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

  1. בתוך קוד קומפוננטה אני קורא ל useSelector כדי "לחבר" בין קוד הקומפוננטה לנתיב מסוים באוביקט ה State ב Redux.

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

const todos = useSelector((state: AppState) => state.todos);

כדי לגשת לכל מערך ה todos שיש בסטייט.

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

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

const todosCount = useSelector((state: AppState) => state.todos.length);

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

  1. הספריה Reselect מאפשרת לנו לתאר את הקשר בין Selector-ים שונים, וכך לחסוך חישובים מיותרים. סלקטור יחושב מחדש רק אם חלק מהתלויות שלו השתנו.

בואו נראה עוד דוגמה קצת יותר מורכבת אבל עדיין בעולם של Todos:

import { createSelector } from "@reduxjs/toolkit";
import { AppState } from "./store";

const todos = (state: AppState) => state.todos;

export const todosThatStartWithA = createSelector(todos, (todos) =>
  todos.filter((t) => t.message.startsWith("a"))
);

export const finishedTodosThatStartWithA = createSelector(
  todosThatStartWithA,
  (todos) => todos.filter((t) => t.completed)
);

export const numberOfFinishedTodosThatStartWithA = createSelector(
  finishedTodosThatStartWithA,
  (finishedTodosThatStartWithA) => {
    console.log(`Recalculating the length`);
    return finishedTodosThatStartWithA.length;
  }
);

יש לי כל מיני שאלות שקשורות ל todos, למשל:

  1. מי ה todos במערך שההודעה שלהם מתחילה ב a ?

  2. מי ה todos במערך שההודעה שלהם מתחילה ב a, והם מסומנים בתור completed ?

  3. כמה todos יש במערך שגם ההודעה שלהם מתחילה ב a וגם מסומנים בתור completed ?

השימוש ב createSelector מספק דרך נוחה לשלב כמה Selectors, וגם לחשב מחדש כל Selector רק כשהתלויות שלו באמת משתנות. אבל בואו לא נתבלבל, אפילו Reselect אינו קוסם.

למרות שהכתיב נותן הרגשה כאילו ה Selectors תלויים אחד בשני, בפועל בגלל ששני ה Selectors שבאמצע משתמשים ב filter (ולכן מייצרים מערך חדש), מצב התלויות הוא שכל ה Selectors תלויים ב state.todos. ברגע שמערך ה todos משתנה מכל סיבה שהיא, כל ה Selectors יחושבו מחדש, כי todosThatStartWithA ו finishedTodosThatStartWithA תמיד מחזירים מערך חדש. אם נריץ את הקוד נגלה שכל שינוי ב todos, גם אם ה todos שהשתנו לא מתחילים ב a, עדיין גרם לחישוב מחדש של ה Selector האחרון.

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

הצצה לעולם פוסט GPT3

05/12/2022

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

Implement a program that reads a file and writes its lines backwards

וקיבלתי את הקוד הבא בפייתון:

import sys

def main():
    if len(sys.argv) != 2:
        print("Usage: python reverse.py <file>")
        sys.exit(1)

    file = sys.argv[1]
    with open(file, 'r') as f:
        lines = f.readlines()
        lines.reverse()
        for line in lines:
            print(line, end='')

if __name__ == "__main__":
    main()

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

אז מה, להחליף מקצוע? נדמה לי שהמסקנה היא בדיוק הפוכה:

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

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

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

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

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

create a unix pipeline to sum all the numbers read from stdin

הוא הציע שני רעיונות, כל אחד עם הבעיות שלו:

# Option 1
cat | tr ' ' '\n' | awk '{ sum += $1 } END { print sum }'

# Option 2
cat | paste -sd+ | bc

בראשון ה cut וה tr מיותרים; השני לא עובד על מק ואיתו כנראה על עוד כמה וריאנטים של יוניקס כי paste אצלנו לא קוראת מ STDIN.

ולמרות הכל הסיפור הזה מרשים ומרגש. אם עוד לא קשקשתם עם GPT3 לכו להירשם עכשיו. טירוף מה שהולך שם.

ושוב awk הציל את היום

04/12/2022

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

המשך קריאה

איך קוד מתקלקל (דוגמת טייפסקריפט + ריאקט)

03/12/2022

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

type PinkProps = {
    page?: number;
    itemsPerPage?: number;
    items: Array<{ id: number, text: string }>;
};

function Pink({ page=1, itemsPerPage=25, items }: PinkProps) {
    // component implementation ...
}

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

type OrangeProps = Pick<PinkProps, "page"|"itemsPerPage"> & {
    items: Array<{ id: number, visible: boolean }>
};

function Orange({ page=1, itemsPerPage=25, items}: OrangeProps) {
    // component implementation ...
}

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

function isValid(item: OrangeProps['items'][number]) {
    // validate the item from orange props
}

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

type PinkProps = {
    page?: number;
    itemsPerPage?: number;
    items: Array<{ id: number, text: string }>;
};

type OrangeProps = Pick<PinkProps, "page"|"itemsPerPage"> & {
    items: Array<{ id: number, visible: boolean }>
};

function isValid(item: OrangeProps['items'][number]) {
    // validate the item from orange props
}

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

type Paginated = {
    page?: number;
    itemsPerPage?: number;
}

type TextItem = {
    id: number;
    text: string;
}

type ToggleItem = {
    id: number;
    visible: boolean;
}

type PinkProps = Paginated & {
    items: Array<TextItem>;
};

type OrangeProps = Paginated & {
    items: Array<ToggleItem>;
}

function isValid(item: ToggleItem) {
    // validate the item from orange props
}

function Pink({ page=1, itemsPerPage=25, items }: PinkProps) {}


function Orange({ page=1, itemsPerPage=25, items}: OrangeProps) {}

ארוך יותר, כן, אבל הרבה יותר ברור וקל להרחבה.

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

פיתרון Advent Of Code יום 1 בעזרת יוניקס

02/12/2022

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

המשך קריאה

איפה היית עד עכשיו?

01/12/2022

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

הדבר השני והחשוב יותר הוא להבין למה לא ראית את הדרך הטובה יותר קודם? איפה כדאי לחפש? ואיזה דברים כדאי לשנות בהתנהלות היום יומית כדי להיחשף ליותר דרכים טובות?

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

איתחול רידאקס ממידע שמגיע מהשרת

30/11/2022

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

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

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

המשך קריאה

סיקור באג - הארכת מנוי שלא עבדה

29/11/2022

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

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

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

def subscribe(duration)
    self.subscription_end_date = (Time.zone.now + duration)
end

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

def subscribe(duration)
    self.subscription_end_date = ((subscription_end_date || Time.zone.now) + duration)
end

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

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

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

def subscribe(duration)
    existing_subscription_period = if subscription_end_date.present? && Time.zone.now < subscription_end_date
      subscription_end_date
    else
      Time.zone.now
    end

    self.subscription_end_date = (existing_subscription_period + duration)
end

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

היום למדתי: אירועי mousedown ו mouseup ב JavaScript לא תמיד מתואמים

28/11/2022

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

התוכנית אחרי ניקוי עושה דבר מאוד פשוט - כל פעם שמישהו לוחץ על כפתור של העכבר היא מציגה את הטקסט down, וכשעוזבים את הכפתור היא משנה את הטקסט ל up. אפשר לשחק עם הקוד בקודפן בקישור הזה: https://codepen.io/ynonp/pen/vYrrRPz?editors=1010

או לראות אותו מוטמע כאן:

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

קוד דומה ייכשל גם אם נעבור להשתמש ב Pointer Events.

הנקודה החשובה - קשה לזהות כשמשתמש מפסיק ללחוץ על העכבר. לפי הפיצ'ר שאתם בונים תצטרכו למצוא פיתרון טוב יותר, לדוגמה אני נתקלתי בזה כשתיקנתי מנגנון של Resize בדפדפן, שם אפשר לזהות כשסמן העכבר "יוצא" מאזור ה Resize ולהפסיק את הפעולה, או לתפוס אירועי Mouse Move ולבדוק בכל אירוע כזה איזה כפתורים עדיין לחוצים. עבור משימות אחרות תצטרכו למצוא מעקפים אחרים, אבל באופן כללי אל תבנו על התאמה בין אירועי mousedown ו mouseup.