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

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

הזמנה לוובינר: היכרות עם GraphQL

02/10/2022

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

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

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

  1. למערכת יש מספר לקוחות (למשל Web ו Mobile), וכל אחד מהם צריך להציג מידע שונה, כלומר State-ים שונים.

  2. מאחר וכל State מיוצג ב REST על ידי Endpoint בשרת, אנחנו נשארים לבחור אחת משתי גישות בעייתיות - או שנבנה Endpoints נפרדים בשרת לכל יישום, או שכל יישום יצטרך לגשת לכמה Endpoints כדי למשוך את המידע שהוא צריך.

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

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

ביום חמישי הקרוב בתאריך 6/10 בשעה 10:00 שעון ישראל אעביר וובינר של שעה על GraphQL בו אספר על הפרוטוקול ואראה איך לקחת יישום Node.JS שבנוי בגישת REST ולהפוך אותו ל GraphQL באמצעות כתיבת Schema ו Resolver.

הוובינר יעבור בזום ואתם מוזמנים להירשם ולהצטרף בקישור: https://www.tocode.co.il/workshops/123

קימפול תוכנית Qt ל Web Assembly

01/10/2022

לפני כמה ימים שוחררה גירסה 6.4 של Qt ובעיניי הפיצ'ר הכי מלהיב שם הוא התמיכה המובנית ב Web Assembly. היום מה שכבר קיים ועובד ב Qt הוא:

  1. תמיכה מלאה של Qt Creator ב Web Assembly, כך שאתם יכולים לקחת כל פרויקט Qt שכבר יש לכם (ווידג'טס או QML) ופשוט לקמפל אותו לקובץ wasm.

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

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

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

דמו? ברור. זה קוד Qt שבניתי ב Qt Creator. התוכנית היא בסך הכל קובץ אחד main.cpp שבונה מונה לחיצות:

#include <QApplication>
#include <QtWidgets>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QWidget w;
    auto clicked = 0;
    auto btn = new QPushButton("Add 1");
    auto result = new QLabel(QString("Button clicked %1 times").arg(clicked));
    w.setLayout(new QVBoxLayout());
    w.layout()->addWidget(new QLabel("Hello World"));
    w.layout()->addWidget(result);

    w.layout()->addWidget(btn);

    QObject::connect(btn, &QPushButton::clicked, [&]() {
        clicked++;
        result->setText(QString("Button clicked %1 times").arg(clicked));
    });

    w.show();
    return a.exec();
}

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

אחרי קומפילציה מקבלים קובץ wasm ענקי, שני קבצי js וקובץ html. אפשר לראות את כולם בריפו שיצרתי כאן: https://github.com/ynonp/qt-wasm-demo/

ודמו רץ בגיטהאב פייג'ס בקישור הזה: https://ynonp.github.io/qt-wasm-demo/

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

אם גם אתם רוצים לשחק עם הגירסה כל מה שצריך זה להוריד ולהתקין את Qt מהקישור הזה: https://www.qt.io/download-qt-installer

ולהתקין את Emscripten מהקישור הזה: https://emscripten.org/docs/getting_started/downloads.html

נ.ב. מבחינת רישיון ממה שאני מבין בשביל לבנות אפליקציות מסחריות יש לקנות רישיון מסחרי בגלל שהקומפילציה ל Web Assembly היא סטטית. עושה רושם שהם עובדים על אפשרות לקומפילציה דינמית מה שיאפשר להשתמש ברישיון ה LGPL ולבנות גם יישומים מסחריים בקוד סגור ב Web Assembly.

שלום עולם חלוד

30/09/2022

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

המשך קריאה

קצת כמו כלב רק עם כנפיים

29/09/2022

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

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

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

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

type Upsert<FullType, T> = T extends { id: number }
    ? Partial<FullType>
    : Omit<FullType, "id">;

מאוד רציתי לקחת טיפוס כזה:

type MyStuff = {
    id: number,
    foo: number,
    bar: number,
}

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

function upsert<T>(thing: Upsert<MyStuff, T>) {}

// I wanted these to compile:
upsert({ id: 10, foo: 5 });
upsert({ foo: 10, bar: 20 });

// And this to not compile:
upsert({ foo: 5 });

וכמובן שזה לא עובד.

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

הקוד הזה כבר עובד:

type Upsert<FullType, T> = T extends { id: number }
    ? T & Partial<FullType>
    : T & Omit<FullType, "id">;

type MyStuff = {
    id: number,
    foo: number,
    bar: number,
}

function upsert<T>(thing: Upsert<MyStuff, T>) {}

// Compiles
upsert({ id: 10, foo: 5 });
upsert({ foo: 10, bar: 20 });

// Doesn't compile - missing "id" or "bar"
upsert({ foo: 5 });

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

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

הקפיצה

28/09/2022

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

"כן אני מבין את זה"

"ברור"

"רעיון טוב"

"אני יכול לכתוב את זה"

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

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

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

לאן מזיזים את המחט

27/09/2022

אני לא יודע עוד מה אני רוצה לבנות. אני צריך את הגמישות לשנות תוך כדי תנועה.

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

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

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

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

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

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

אנחנו בונים מחדש את ה Front End כל שנתיים - כל השכל של הכלי הוא בצד השרת.


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

כשהכלי לא מספק את התמורה

26/09/2022

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

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

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

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

הבעיה עם טריקים זולים

25/09/2022

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

הבעיה עם טריקים זולים היא בדיוק שהם עובדים. אבל רק פעם או פעמיים.

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

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

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

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

טיפ טייפסקריפט: חתימה של פונקציה מתוך Literal Types

24/09/2022

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

type Event =
  | { type: 'login', payload: { username: string }}
  | { type: 'logout'}
  | { type: 'sendMessage', payload: { to: string, text: string }};

function handle(event: Event) {}

ועכשיו שורה כזאת תתקמפל:

handle({ type: 'login', payload: { username: 'yay' }});

אבל שורה כזאת לא תתקמפל:

handle({ type: 'login', payload: { to: 'yay', text: 'abc' }});

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

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

ניסיון ראשון עשוי להיראות כך:

type Event =
  | { type: 'login', payload: { username: string }}
  | { type: 'logout'}
  | { type: 'sendMessage', payload: { to: string, text: string }};

function handle(eventType: Event["type"], eventPayload: Event["payload"]) {}

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

type Event =
  | { type: 'login', payload: { username: string }}
  | { type: 'logout', payload: undefined }
  | { type: 'sendMessage', payload: { to: string, text: string }};

function handle(eventType: Event["type"], eventPayload?: Event["payload"]) {}

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

handle("login");

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

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

type Event =
  | { type: 'login', payload: { username: string }}
  | { type: 'logout', payload: undefined }
  | { type: 'sendMessage', payload: { to: string, text: string }};

function handle<E extends Event, EventType extends E["type"]>(
  eventType: EventType,
  eventPayload: Extract<Event, { type: EventType }>["payload"]
) {}

handle("login", { username: "ynon" });
handle("logout", undefined);
handle("sendMessage", { to: "ynon", text: "hi ;)"});

עכשיו שלושת הקריאות מתקמפלות, אבל קריאות שלא מתאימות לחתימה לא יתקמפלו. למשל זה לא יעבור:

handle("login", { to: "me", text: "bye" });

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

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

function handle<E extends Event, EventType extends E["type"]>(
  eventType: EventType,
  eventPayload: Extract<Event, { type: EventType }>["payload"] extends undefined
    ? "MAKE EVENT PAYLOAD OPTIONAL"
    : "USE THE VALUE FROM EXTRACT ..."
) {}

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

type Event =
  | { type: 'login', payload: { username: string }}
  | { type: 'logout', payload: undefined }
  | { type: 'sendMessage', payload: { to: string, text: string }};

function handle<
  E extends Event,
  EventType extends E["type"],
  EventPayload extends Extract<Event, { type: EventType }>["payload"]
>(
  eventType: EventType,
  ...eventPayload: EventPayload extends undefined
      ? [undefined?] 
      : [EventPayload]
) {}

handle("login", { username: "ynon" });
handle("logout");
handle("sendMessage", { to: "ynon", text: "hi ;)"});

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

ירושה? לא בבית ספרנו

23/09/2022

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

המשך קריאה