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

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

מה שעשינו פעם קודמת

07/04/2024

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

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

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

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

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

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

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

כן, באמת משתמשים בזה

06/04/2024

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

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

type DeepPartial<T> = T | 
(T extends Array<infer U> 
    ? DeepPartial<U>[] 
    : T extends Map<infer K, infer V> 
        ? Map<DeepPartial<K>, DeepPartial<V>>
        : T extends Set<infer M>
            ? Set<DeepPartial<M>>
            : T extends object ? {
                [K in keyof T]?: DeepPartial<T[K]>;
            } : T);

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

בואו נקרא את ההגדרה יחד-

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

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

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

  4. גם לא מפה? לא להיבהל אולי זה Set. אם כן אז בעצם אנחנו צריכים סט מטיפוס DeepPartial של הפריטים ב T.

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

  6. לא אוביקט? טוב אז נשארים עם T כי זה בטח משתנה פשוט.

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

הבחירה היא לא בין הצלחה לכישלון

05/04/2024

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

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

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

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

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

04/04/2024

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

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

המשך קריאה

דינו או באן

03/04/2024

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

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

בכל ההשוואות שמצאתי בין שלושת הכלים באן היה המהיר ביותר, עם יעד להיות כמה שיותר תואם ל node. בגירסה 1.1 שיצאה עכשיו הם מספרים בהתלהבות על ה APIs הלא מתועדים של node שהם משכפלים.

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

לאן זה הולך?

קשה לי לדמיין עתיד בו node.js ממשיך להוביל. אם באן יעבדו נכון מהר מאוד אנשים יתחילו להחליף את ה node שלהם ב bun בלי להרגיש בהבדל. ובדיוק כמו שאף אחד לא מתגעגע לוובפאק גם לא נראה געגועים ל node. הדבר החדש יעבוד בדיוק אותו דבר רק מהר יותר.

ועדיין אני מקווה לראות את דינו לוקח את ההובלה. יש להם קהילה מפרגנת ומיתוג טוב וכוונה אמיתית ליצור משהו טוב יותר. אם זה יקרה זה לא יהיה Drop In Replacement אלא בחירה חדשה שנצטרך לעשות. אני אשמח להתעורר יום אחד ולגלות שכמו שלא הייתי צריך יותר את jQuery אני גם לא אצטרך את כל ה require, ה Buffer וה APIs הלא מתועדים.

אבל אני לא משתמש ב xz

02/04/2024

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

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

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

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

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

קבצים סטטיים מאקספרס על Deno Deploy

01/04/2024

אם תהיתם מאיפה הגיעו כל הפוסטים על node ו deno בימים האחרונים אז תשמחו לשמוע שאני עובד עכשיו על רענון קורס Node שבאתר ומקווה שעד פסח אתם תקבלו קורס מעודכן על Node שכבר כולל אינסוף דוגמאות קוד ב TypeScript ותואם גם ל Node וגם ל Deno. רוב הזמן זה לא נורא מסובך אבל בלי קצת בעיות תאימות החיים לא היו מעניינים. בפוסט היום אני רוצה לדבר על בעיית תאימות קטנה כזאת בשירות Deno Deploy.

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

בהינתן תיקיית פרויקט Deno אפשר לכתוב משורת הפקודה:

$ deployctl deploy

והפרויקט עולה לאוויר.

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

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

את קוד הפרויקט אתם יכולים למצוא בגיטהאב שלי בקישור: https://github.com/ynonp/deno-deploy-files

כאן אציג רק את עיקרי הדברים.

המשך קריאה

כוונות טובות, ביצועים גרועים

30/03/2024

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

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

g
  .V()
  .has(VertexLabels.Card, Properties.IndexedLabel, VertexLabels.Card)
  .where(__.and(
    __.out(EdgeLabels.Front).hasId(front.entityId),
    __.out(EdgeLabels.Back).hasId(back.entityId)))
  .fold()
  .coalesce(
    __.unfold(),
    __.addV(VertexLabels.Card)
      .as("card")
      .addTimestampsProperties()
      .property(Properties.IndexedLabel, VertexLabels.Card)
      .asCard()
      .addE(EdgeLabels.Front).to(__.V(front.entityId))
      .select("card")
      .addE(EdgeLabels.Back).to(__.V(back.entityId))
      .select("card"))
  .id()
  .next()

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

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

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

    val id = g
      .V(front.entityId)
      .coalesce(
        __.in(EdgeLabels.Front).where(__.out(EdgeLabels.Back).hasId(back.entityId)),
        __.addV(VertexLabels.Card)
          .as("card")
          .addTimestampsProperties()
          .property(Properties.IndexedLabel, VertexLabels.Card)
          .asCard()
          .addE(EdgeLabels.Front).to(__.V(front.entityId))
          .select("card")
          .addE(EdgeLabels.Back).to(__.V(back.entityId))
          .select("card")
      ).id()
      .next()

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

כשהתקן עובד לרעתך

29/03/2024

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

import { readLines } from "https://deno.land/std@0.221.0/io/read_lines.ts";
import * as path from "https://deno.land/std@0.221.0/path/mod.ts";

const filename = path.join(Deno.cwd(), "std/io/README.md");
let fileReader = await Deno.open(filename);

for await (let line of readLines(fileReader)) {
  console.log(line);
}

מה שחשוב זה הלולאה בסוף שקוראת קובץ שורה אחר שורה ומאפשרת לטפל בכל שורה בנפרד.

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

ועכשיו השאלה - האם לשתף פעולה עם הקידמה ולכתוב לבד מנגנון שובר שורות, להישאר עם המנגנון ה Deprecated או בכלל להשתמש במודול readline של node, שגם נטען בקלות מתוכנית דינו?

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

נ.ב. ככה זה נראה כשקוראים קובץ שורה אחרי שורה עם ה Web Streams API:

const file = await Deno.open("a.js", { read: true });
const readableStream = file.readable.pipeThrough(new TextDecoderStream()).pipeThrough(new TransformStream({
  transform: (chunk, controller) => {
    const lines = chunk.split("\n");
    for (const line of lines) {
      if (line) {
        controller.enqueue(line);
      }
    }
  },
}));

for await (const line of readableStream) {
  console.log(`> ${line}`);
}

וזאת הגירסה עם readline של node:

import readline from 'node:readline';
import fs from 'node:fs';

const myName = "a.js";

const rl = readline.createInterface({
  input: fs.createReadStream(myName),
})

let index = 0;
rl.on('line', (line) => {
  index += 1;
  console.log(`${String(index).padStart(2, '0')} ${line}`);
});

מה דעתכם? איזה גירסה הייתם בוחרים? ולמה?