ru
Feedback
ToCode

ToCode

Открыть в Telegram

טיפים קצרים למתכנתים מאת ינון פרק

Больше
1 419
Подписчики
Нет данных24 часа
Нет данных7 дней
-530 день
Архив постов
ToCode
1 420
שלוש תחנות בדרך למציאת עבודה בהייטק קודם כל גילוי נאות, בתור פרילאנסר ב 12 שנים האחרונות עבר הרבה זמן מאז הפעם האחרונה שחיפשתי עבודה. אני היום מכיר את העולם הזה יותר דרך חברים או מניסיונות שלי לעזור ללקוחות לגייס. ובכל זאת מנקודת המבט הקצת מרוחקת שלי יש שלושה עקרונות אוניברסליים שאני רואה בכל תהליך של חיפוש או גיוס. עקרונות שכשאתם מבינים אותם התמונה של חיפוש עבודה נראית הרבה פחות מלחיצה. בניית תשתית העיקרון הראשון הוא בניית התשתית. תשתית במובן של חיפוש עבודה מורכבת מהיכרות עם אנשים רלוונטים בתחום, שיפור הרמה המקצועית שלכם, בניית תיק עבודות שמראה על יכולת מקצועית ובניית מיומנויות רכות שיאפשרו לכם להשתלב בארגון. הפרטים כאן הם פחות חשובים וקל למצוא המון טיפים ברשת לזה. בגדול בשביל להכיר אנשים כדאי לשלוח קורות חיים לחברות השמה ולצלצל למגייסות שם כדי להבין מה כדאי לשפר (וכך להתיידד עם אותם אנשים בחברות ההשמה), להשתתף באירועים מקצועיים של התעשייה, ללכת להאקתונים, להיכנס לקהילות ברשת ולהשתתף בדיונים שם. כל דבר שעוזר לכם להכיר אנשים מעניינים מהתעשייה ולהם להכיר אתכם. בשביל להשתפר מקצועית חשוב כל הזמן ללמוד. יש היום המון קורסים ברשת, הרצאות מקצועיות מכל כנס בעולם זמינות ביוטיוב, יותר ספרים ממה שאפשר לקרוא בחיים שלמים. עשו לעצמכם הרגל כל יום ללמוד משהו חדש כי יש באמת המון המון דברים שתצטרכו ללמוד. מתישהו תתחילו להיתקל בהזדמנויות לעבודה ואז תרצו להראות שיש לכם את היכולת לבצע את מה שצריך. פרויקטים טובים לתיק עבודות לא בונים ביום אחד ועדיף להתחיל לעבוד עליהם כמה שיותר מוקדם. האתגרים בשלב הזה הם שאנחנו לא יודעים כמה זמן הוא ייקח ואין לנו דרך לקבל פידבק אמיתי שישקף לנו כמה טוב אנחנו מתקדמים. יום אחד יתחילו להגיע הזדמנויות. עד שזה יקרה הדבר היחיד שאפשר וצריך לעשות הוא להמשיך לעבוד. ניצול הזדמנויות כשיתחילו להגיע הזדמנויות חשוב לשים לב אליהן, לא לפספס ולהיות מאוד ברורים לגבי הכוונות שלכם והיכולות שלכם. פתאום מישהו מצלצל ורוצה להזמין אותך לראיון למשרת פיתוח Full Stack. או שבשבוע אחד הטלפון פתאום לא מפסיק לצלצל וכולם רוצים שתבואו לעבוד אצלם על פרויקטים של מטבעות וירטואליים. זה יכול להיות כל דבר. חשוב להבין מאוד מוקדם מה בדיוק אתם מחפשים כדי להיות ערניים להזדמנויות הנכונות ולא ללכת שולל אחרי דברים לא רלוונטים. אנשים שמנצלים הזדמנויות לא מתלבטים. כשמגיע פרויקט רלוונטי עשיתם כבר מספיק שיעורי בית כדי לדעת שזה פרויקט טוב ולוקחים אותו בלי להסס. האתגר בשלב ההזדמנויות הוא שאנשים שוכחים שהזדמנויות באות בתקופות. שבוע הטלפון לא מפסיק לצלצל ואז חודשיים אין שום הצעה. אל תתנו לעודף ההזדמנויות לבלבל אתכם. התפשרות מצאתם את ההזדמנות לפרויקט טוב? רוב הסיכויים שהוא לא יהיה מושלם. המרחק, המשכורת, הטכנולוגיה, האנשים, השעות, האופציות, ... החלק האחרון הדרוש בשביל למצוא עבודה הוא הנכונות להתפשר על הדברים הפחות חשובים בשביל לקבל את ההזדמנות שחיכיתם לה. שימו לב לתחנות האלה בחיפוש העבודה הבא שלכם ותנו לעצמכם זמן, ואני בטוח שהתהליך יהיה פחות מלחיץ ויותר פרודוקטיבי.

ToCode
1 420
דינו או באן כמו כל תוכנה שהיתה איתנו הרבה זמן, גם 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 הלא מתועדים.

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

ToCode
1 420
קבצים סטטיים מאקספרס על Deno Deploy אם תהיתם מאיפה הגיעו כל הפוסטים על 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 כאן אציג רק את עיקרי הדברים. מצב פיתוח על המכונה שלי במצב פיתוח על המכונה שלי הגשת קבצים סטטיים עובדת בצורה מובנית באקספרס. זה הקוד:
import express, {Request, Response, NextFunction} from 'express';
import process from 'node:process';
import root from './routes/root.ts';
import path from 'node:path';
import url from 'node:url';
import createError from 'http-errors';

const PORT = process.env.PORT || 3000;
const app = express();

const dirname = path.dirname(url.fileURLToPath(import.meta.url));
app.set('view engine', 'ejs');
app.set('views', path.join(dirname, '/views'));

app.use('/', root);
app.use(express.static('./public'));

app.use((req: Request, res: Response, next: NextFunction) => {
  next(createError(404, 'Not Found'));
})

app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  console.error(err);
  res.status(500).send({ errors: [{ message: "Something went wrong" }] });
});

app.listen(PORT, () => {
  console.log(\Server listening on port ${PORT}\);
});
כאשר השורה הרלוונטית היא:
app.use(express.static('./public'));
מה קורה כשדוחפים ל Deno Deploy אחרי העלאה ל Deno Deploy התוכנית עובדת אבל הקובץ הסטטי (בדוגמה שלי זה קובץ CSS) לא נטען. בדיקה בלוג מראה את השגיאה הבאה:
TypeError: Cannot read properties of null (reading 'toUTCString')
    at SendStream.setHeader (file:///node_modules/.deno/send@0.18.0/node_modules/send/index.js:874:31)
    at SendStream.send (file:///node_modules/.deno/send@0.18.0/node_modules/send/index.js:620:8)
    at onstat (file:///node_modules/.deno/send@0.18.0/node_modules/send/index.js:725:10)
    at Deno.stat.then.denoErrorToNodeError.syscall (ext:deno_node/_fs/_fs_stat.ts:85:32)
    at eventLoopTick (ext:core/01_core.js:153:7)
פה כבר התחלתי לפחד. זה נראה ש stat של דינו לא עובד בדיוק כמו stat של node.js ולכן משהו נשבר בשידור הקובץ. במקום לריב איתם הלכתי לכתוב מידלוור משלי שישלח את הקובץ בלי stat. הוא נראה ככה:
app.use(async (req: Request, res: Response, next: NextFunction) => {
  try {
    const userInput = path.join('./public', req.path);
    const safeInput = path.normalize(userInput).replace(/^(\.\.(\/|\\|$))+/, '');
    const mime = contentType(path.extname(safeInput))
    console.log(\File: ${safeInput}. content type = ${mime}\);
    const content = await Deno.readFile(safeInput);
    res.set('Content-Type', mime);
    res.send(buffer.Buffer.from(content));  
  } catch (err) {
    console.log(\file not found ${req.path}\);
    next();
  }
});
ואת התוצאה אפשר לראות אונליין על Deno Deploy עם ה CSS בקישור: https://ynonp-deno-deploy-files-yfge7qm6f97y.deno.dev/

ToCode
1 420
בהעתקת קבצים אני מעדיף לוותר על ה Text Decoding כי בצורה כזאת אני מעתיק את הביטים כמו שהם מקובץ המקור בלי קשר אם הם מייצגים טקסט או לא או מה הקידוד שלהם. למידע נוסף על Streams וכל מה שאפשר לעשות איתם ב Node.JS שווה להעיף מבט בתיעוד: https://nodejs.org/api/webstreams.html כל הדוגמאות בפוסט הזה נבדקו ועובדות ב Node גירסה 21.7 אחרי ששמרתי את הקוד בקובץ בשם a.mjs.

ToCode
1 420
קריאה וכתיבה לקבצים עם Streams ב Node.JS עבודה עם מידע בינארי דרך Streams היא חלק מהתקן של JavaScript ונתמכת בצורה מלאה גם ב Node וגם ב Deno, ואלה חדשות טובות כי תמיד כיף שדברים עובדים בכל מקום. בואו נדבר על הקוד. קצת תיאוריה ברמה הבסיסית Streams באים בשני טעמים יש את ה ReadableStream שאיתו אנחנו קוראים מידע ואת ה WritableStream שאיתו כותבים מידע. ב node.js המודול stream יודע להפוך Streams של Node ל Streams של Web API. הדברים הטובים שאפשר לעשות עם Streams הם: 1. אפשר לחבר אותם אחד לשני. אם מחברים Stream לקריאה ל Stream של כתיבה אז מקבלים העתקה. 2. אפשר להוסיף טרנספורמציות על Stream וככה לקבל Stream חדש. לדוגמה אם לוקחים Stream בינארי של מידע שמגיע לקובץ ומוסיפים טרנספורמציה של פיענוח הביטים לטקסט מקבלים Stream של מחרוזות. דוגמאות? בשמחה. קריאת ביטים מקובץ והדפסתם ב Chunk-ים נתחיל עם קריאת קובץ בינארי לפי בלוקים דרך Stream לקריאה. הפונקציה fs.createReadStream של Node יוצרת Stream לקריאה של Node, ובעזרת stream.toWeb נהפוך אותו ל Stream סטנדרטי. הקריאה מזרם לקריאה היא פשוט איטרציה אסינכרונית ולכן נוכל לכתוב:
import fs from 'node:fs';
import stream from 'node:stream';

const sin = stream.Readable.toWeb(fs.createReadStream('text.md'));
for await (const chunk of sin) {
  console.log(chunk);
}
והפלט:
Uint8Array(15143) [
   35,  32, 215, 156, 215, 162, 215, 169, 215, 149, 215, 170,
   32, 215, 144, 215, 170,  32, 215, 148, 215, 144, 215, 153,
  215, 158, 215, 153, 215, 153, 215, 156,  32, 215, 160, 215,
  164, 215, 156, 215, 144,  32, 215, 169, 215, 149, 215, 145,
   10, 215, 144, 215, 153, 215, 158, 215, 153, 215, 153, 215,
  156,  32, 215, 148, 215, 149, 215, 144,  32, 215, 155, 215,
  160, 215, 168, 215, 144, 215, 148,  32, 215, 144, 215, 151,
  215, 147,  32, 215, 148, 215, 158, 215, 167, 215, 149, 215,
  158, 215, 149, 215,
  ... 15043 more items
]
קריאת טקסט מקובץ באמצעות טרנספורמציה הקריאה מקובץ דרך Stream עבדה ואיפשרה לקרוא את הקובץ בבלוקים. לקבצים בינאריים אפשר לעצור כאן, אבל אם הביטים שבקובץ מכילים טקסט אולי נרצה גם לפענח אותו ולראות מחרוזות על המסך. בשביל זה בדיוק התקן של Web Streams הוסיף מחלקה בשם TextDecoder. השימוש בה מאוד פשוט, אנחנו יוצרים מפענח, בבנאי אפשר להעביר לו את הקידוד (ברירת המחדל היא utf8), ואז מפעילים את הפונקציה decode עם בלוק של ביטים בשביל לפענח אותם לטקסט. ואל תדאגו הוא יודע לטפל כמו שצריך ב Multi Byte Strings אפילו בין Chunk-ים. בשביל שהתוכנית הקודמת תדפיס טקסטים במקום ביטים צריך רק להעביר את הביטים שלנו דרך כזה Decoder. למזלנו ה API של Streams מספק דרך קלה לעשות את זה בדמות מחלקה בשם TextDecoderStream:
import fs from 'node:fs';
import stream from 'node:stream';

const sin = stream
  .Readable
  .toWeb(fs.createReadStream('/etc/shells'))
  .pipeThrough(new TextDecoderStream());

for await (const chunk of sin) {
  console.log(chunk);
}
כתיבה לקובץ עם WritableStream בכתיבה לקובץ יש כמה נקודות שצריך להכיר אבל בעיקרון אין הפתעות גדולות: 1. פותחים WritableStream ומפעילים את הפונקציה getWriter שלו. 2. מפעילים את פונקציית write של ה Writer שקיבלנו בשביל לכתוב. 3. בסוף קוראים ל Close שסוגר את ה Writer ואת ה Stream. הקוד הבא כותב ביטים לקובץ:
import fs from 'node:fs';
import stream from 'node:stream';

const sout = stream
  .Writable
  .toWeb(fs.createWriteStream('demo.bin'));

const writer = sout.getWriter();
const data = new Uint8Array(10).fill(0).map((_, i) => i);
await writer.write(data);
writer.close();
העתקה עם Pipe הטריק האחרון להיום עם Streams הוא העתקת קבצים, והמשחק הוא כזה - במקום להריץ לולאה שתקרא בצורה מפורשת בלוקים ותכתוב אותם לזרם השני אפשר פשוט לחבר זרם לקריאה לזרם לכתיבה והכל מסתדר. זה נראה ככה:
import fs from 'node:fs';
import stream from 'node:stream';

const sin = stream
  .Readable
  .toWeb(fs.createReadStream('/etc/shells'));

const sout = stream
  .Writable
  .toWeb(fs.createWriteStream('shells.txt'));

sin.pipeTo(sout);

ToCode
1 420
כוונות טובות, ביצועים גרועים בבוט אוצר המילים שאני כותב שכבת המידע יחסית פשוטה - הבוט שומר כרטיסיות מילים כשבכל כרטיסיה יש מילה מקדימה ומילה מאחורה ואז הוא יכול לשאול חידונים על המילים האלה. בשביל לא ליצור כפילויות כל פעם שמוסיפים תרגום חדש הבוט בודק בבסיס הנתונים אם יש כבר כרטיסיה עם המילים האלה, ויוצר רק אם מדובר בחיבור חדש. זאת היתה השאילתה שלוקחת מילים ויוצרת כרטיסיה או מחזירה את הכרטיסיה הקיימת:
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. גרמלין נותן הכי הרבה שליטה באיך מבוצעת השאילתה, ומאפשר מאוד בקלות להתאים את אופן סריקת הגרף למודל הנתונים הספציפי של המערכת.

ToCode
1 420
כשהתקן עובד לרעתך בגירסאות ישנות של הספריה הסטנדרטית של דינו היה מודול בשם 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}\);
});

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

ToCode
1 420
נו זה מתחיל פשוט עם לולאת for, ואז מגיעה שורת המחץ - אני לוקח כל שם קובץ ומעביר אותו לפקודה מובנית בשפה שנקראת parse שמפענחת את המחרוזת לשני משתנים (במקרה שלי n ו name, עם מקף ביניהם). אחרי זה מתחילים לשחק עם המידע, הופכים את n למספר, מעלים אותו ב-1 ואז מחברים את כל העמודות למחרוזת אחת אותה אני שומר במשתנה newname. שורה אחרונה של הלולאה היא ה mv שמשנה את שם הקובץ. שכפול סקריפט לעצמו עד לפה הייתי בטוח שהגעתי ל Shell שיודע לעשות הכל אבל אז הגיעה המשימה השלישית שם nushell קצת אכזב אותי. המטרה שלנו היא לכתוב סקריפט שמקבל בשורת הפקודה מספר שמות של קבצים ומעתיק את עצמו לקבצים ששמותיהם עברו כפרמטרים. הבעיה? ל nushell אין מקבילה למשתנה $0 של Shell-ים קלאסיים ולכן אני צריך להסתמך על זה שאני יודע מה שם הקובץ. בהנחה ששם הקובץ הוא clone.nu הקוד הבא עובד:
def main [...destinations] {
    let sourceFile = $env.FILE_PWD + "/clone.nu"
    for output in $destinations {
        cp $sourceFile $output
    }
}
סך הכל nushell נראה כמו פרויקט מבטיח - הוא עובד מהר ועבודה עם מידע במקום עם טקסט יכולה לחסוך טעויות בתוכניות מורכבות.

ToCode
1 420
שלושה סקריפטים ראשונים ב nushell כדי ללמוד איך זה עובד נו-של הוא סוג חדש של Shell, הוא כתוב ב rust ומביא את הגישה של Powershell - לפיה כל דבר הוא data - גם ליוניקס. בואו נכתוב שלושה סקריפטים ראשונים ב nushell כדי ללמוד איך זה עובד ולחשוב אם הוא מתאים. הצגת טקסט על ערך מויקיפדיה את nushell אפשר להתקין לפי ההוראות באתר שלהם: https://www.nushell.sh/ הכי קל באמצעות הפעלת:
brew install nushell
אחרי ההתקנה מפעילים nu כדי להיכנס ל Shell ומספיק להפעיל ls כדי להבין שאנחנו כבר לא בקנזס. זה הפלט:
ls                                                                                                                                                  27/03/24 18:00:15
╭───┬───────────┬──────┬───────┬────────────────╮
│ # │   name    │ type │ size  │    modified    │
├───┼───────────┼──────┼───────┼────────────────┤
│ 0 │ 02-one    │ dir  │  64 B │ 2 hours ago    │
│ 1 │ 03-two    │ dir  │  64 B │ 2 hours ago    │
│ 2 │ 04-three  │ dir  │  64 B │ 2 hours ago    │
│ 3 │ carlos.py │ file │ 643 B │ 35 minutes ago │
│ 4 │ clone.nu  │ file │ 153 B │ 2 hours ago    │
│ 5 │ inc.nu    │ file │ 229 B │ an hour ago    │
╰───┴───────────┴──────┴───────┴────────────────╯
סימן הקו אנכי מתפקד פחות או יותר כמו שתפקד ב Shell-ים הקלאסיים, אבל עכשיו בגלל שהכל הוא data הרבה יותר קל לעבוד עם המידע, לדוגמה בשביל להדפיס רק את שם הקובץ והגודל נכתוב:
ls | select type size

╭───┬──────┬───────╮
│ # │ type │ size  │
├───┼──────┼───────┤
│ 0 │ dir  │  64 B │
│ 1 │ dir  │  64 B │
│ 2 │ dir  │  64 B │
│ 3 │ file │ 643 B │
│ 4 │ file │ 153 B │
│ 5 │ file │ 229 B │
╰───┴──────┴───────╯
סקריפט ב nushell מסתיים בסיומת nu. לא הצלחתי להתקין את התוסף ל vim שלהם אבל התוסף ל VS Code עבד לי מצוין. כמו שהבטחתי שלושה סקריפטים בשביל התחלה, הראשון מקבל שם של ערך מויקיפדיה ומציג את התוכן:
def main [value] {
    let url = $"https://en.wikipedia.org/w/api.php?action=query&prop=revisions&titles=($value)&rvslots=*&rvprop=content&formatversion=2&format=json"
    http get $url | get query.pages.0.revisions.0.slots.main.content
}
איך זה עובד? אז סיפרתי כבר שכל דבר זה Data, ולכן גם JSON שמגיע מהרשת הוא מידע שאפשר לעבוד איתו. הפקודה http get פונה לרשת למשוך מידע והפקודה get מציגה רק חלק מהטבלה. במקרה של ויקיפדיה ה JSON מכיל אוביקטים מקוננים והמחרוזת:
query.pages.0.revisions.0.slots.main.content
היא הנתיב בתוך האוביקט שמכיל את הערך. אם תתקינו nushell ותשמרו את הסקריפט בקובץ בשם wiki.nu תוכלו להפעיל אותו באופן הבא כדי להציג ערך מתוך ויקיפדיה:
nu wiki.nu Pet_door
שינוי שם של כל הקבצים שמתחילים במספר אתגר שני שרציתי לנסות בשביל ללמוד על nushell היה לשנות את השמות של כל הקבצים ששמם מתחיל במספר באמצעות העלאת המספר ב-1, כלומר אם היו לי בתיקיה הקבצים:
ls [0-9]* 

27/03/24 18:07:58
╭───┬──────────┬──────┬──────┬─────────────╮
│ # │   name   │ type │ size │  modified   │
├───┼──────────┼──────┼──────┼─────────────┤
│ 0 │ 02-one   │ dir  │ 64 B │ 2 hours ago │
│ 1 │ 03-two   │ dir  │ 64 B │ 2 hours ago │
│ 2 │ 04-three │ dir  │ 64 B │ 2 hours ago │
╰───┴──────────┴──────┴──────┴─────────────╯
אז אחרי הפעלת הסקריפט אני מקבל:
ls [0-9]*

╭───┬──────────┬──────┬──────┬─────────────╮
│ # │   name   │ type │ size │  modified   │
├───┼──────────┼──────┼──────┼─────────────┤
│ 0 │ 03-one   │ dir  │ 64 B │ 2 hours ago │
│ 1 │ 04-two   │ dir  │ 64 B │ 2 hours ago │
│ 2 │ 05-three │ dir  │ 64 B │ 2 hours ago │
╰───┴──────────┴──────┴──────┴─────────────╯
וקוד הסקריפט:
for f in (ls [0-9]* | get name) {
  let newname = $f | parse '{n}-{name}' | into int n | update n {|row| $row.n + 1 } | each {|row| [($row.n | fill -a right -c '0' -w 2), "-", $row.name] | str join } | get 0
  mv $f $newname
}

ToCode
1 420
גרמלין - סיכום ניסוי בחודשים האחרונים בניתי בוט לטלגרם שהשתמש בבסיס נתונים גרפי. מאוד נהניתי מהמחקר על בסיסי נתונים גרפיים ולמדתי על המון סוגים שלהם אבל בשורה התחתונה לא עפתי עליהם וכנראה שבפרויקט הבא כבר אחזור לפוסטגרס. אני מסכם כאן את הנקודות המרכזיות מהניסוי. גרמלין, ניאו או דטומיק בדקתי שלושה בסיסי נתונים גרפיים - neo4j, datomic ו TinkerPop עד שבחרתי בטינקר, וגם זה היה בשיטת האלימינציה. לשפת השאילתות של דטומיק לא התחברתי. ניאו היה מסחרי מדי וככה נשאר טינקרפופ שהוא בכלל Spec לשפת שאילתות שנקראת גרמלין. את גרמלין לוקח זמן ללמוד אבל בסופו של דבר היא מעולה. כתבתי עליה כאן: https://www.tocode.co.il/blog/2023-10-gremlin-first-steps וכאן: https://www.tocode.co.il/blog/2023-10-dsl-gremlin-queries אבל הבעיה ב Spec זה שצריך בסוף להריץ את זה מול בסיס נתונים מסוים. יש המון בסיסי נתונים שמריצים גרמלין והם מאוד שונים ביניהם, וגם האופן בו הם מריצים גרמלין לא תמיד זהה. זה אומר שיכול להיות ששאילתות יעבדו או יעבדו בצורה יחסית מהירה על בסיס נתונים אחד אבל לא יעבדו על בסיס נתונים אחר. עבור הבוט שלי בחרתי בסוף ב JanusGraph שנראה ממש טוב בבדיקות מקומיות ויכול לעבוד גם In Memory למצב בדיקות, גם מול מערכת קבצים וגם מול בסיס נתונים מבוזר כמו קסנדרה. בעיה 1 - תיעוד התיעוד באתר של אפאצ'י מאוד מפורט, אבל עדיין אין המון תיעוד ברשת מחוץ לאתר הרשמי, דיונים בפורומים מיושנים ולכן כלי AI לא יודעים לעזור בכתיבת שאילתות. התוצאה היא עקומת לימוד יותר קשה וערבוב בקוד בין שאילתות שכתבתי כשממש לא ידעתי גרמלין לשאילתות יותר רציניות שכתבתי כשכבר הבנתי איך זה עובד. גם ברמת התפעול מאוד קשה למצוא מידע על JanusGraph מחוץ לתיעוד הרשמי, והרבה פעמים אתה נתקע עם בעיות וצריך לחפור ב Issues בגיטהאב ובגוגל קבוצות בשביל למצוא כיוונים. בעיה 2 - זיכרון הדבר השני שלא סיפרו לי על JanusGraph זה שהוא זולל זיכרון. יש שמועה שזה בגלל שהוא כתוב ב Java, ובמקומות אחרים אומרים שאפשר לצמצם את צריכת הזיכרון. אני לא יודע. התוצאה בפועל היתה צריכת זיכרון מאוד גבוהה עבור Cache-ים גם כשבסיס הנתונים לא היה גדול, בטח בהשוואה לבסיסי נתונים טבלאיים (8 ג'יגה זיכרון בשביל בסיס נתונים למשחקים). בעיה 3 - אינדקסים הם קריטיים תכונה שלישית של JanusGraph שגיליתי תוך כדי תנועה (טוב זה הכיף בניסויים כאלה) היא שאפשר לגשת לנתונים רק דרך אינדקס. טוב זה לא מדויק, תמיד אפשר לכתוב שאילתת גרמלין שלא תעבור דרך אינדקס, זה פשוט שהשאילתה לא תסתיים אף פעם. נכון גם בבסיסי נתונים טבלאיים הייתי צריך להגדיר אינדקסים אבל זה רק כשהיה המון מידע או בשאילתות מורכבות. ומה שיותר מרגיז ספציפית ב JanusGraph זה שיש ממש טקס להגדרה ושינוי של אינדקס וכל אות לא נכונה שכותבים יכולה לקלקל את הכל ואז אתה נתקע עם איזה אינדקס במצב חצי אפוי. סיכום בשורה התחתונה בסיסי נתונים גרפיים עדיין נראים לי כמו רעיון טוב אבל לפחות בחוויה שלי המימוש היה מאכזב. נפטון של אמזון (גם תומך גרמלין) היה מאוד יקר גם כששמרתי עליו מעט מידע, והרבה אפשרויות מאלה שמופיעות באתר של גרמלין לא עבדו או היו מסחריות מדי. אחסון מקומי של JanusGraph דרש הרבה יותר התעסקות בהגדרות בהשוואה לפוסטגרס ועקומת הלימוד בכתיבת השאילתות היתה לא מבוטלת. יש גם פחות כלים גרפיים בהשוואה לעבודה עם בסיסי נתונים טבלאיים. אני כנראה לא אבחר בבסיס נתונים גרפי לפרויקט הבא שלי, מה דעתכם? יצא לכם לעבוד עם בסיסי נתונים גרפיים? איך היו החוויות שלכם? אפשר לשתף בטלגרם או בתגובות כאן.