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

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

קבצים סטטיים מאקספרס על 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}`);
});

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

גרמלין - סיכום ניסוי

27/03/2024

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

המשך קריאה

שני תרגילי עוקץ לכבוד פורים

25/03/2024

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

המשך קריאה

כשאני לא יודע לכתוב Type Hint בפייתון

24/03/2024

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

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

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

def random_weighted_item(items):
    items = sorted(items, key=lambda x: x.weight)
    min_weight = min(i.weight for i in items)
    max_weight = max(i.weight for i in items)
    normalized_weights = [(i.weight - min_weight) / (max_weight - min_weight) for i in items]
    cumulative_sum = list(accumulate(normalized_weights))
    randomized_weight = random.random() * cumulative_sum[-1]
    index = next(i for i, e in enumerate(cumulative_sum) if e > randomized_weight)
    return items[index]

אפשר להוסיף לזה בקלות Type Hints בעזרת Type Var וזה יראה כך:

class HasWeight(Protocol):
    weight: int

T = TypeVar("T", bound=HasWeight)

def random_weighted_item(items: list[T]) -> T:
    ...

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

def random_weighted_item(items: list[T], weight: Callable[[T], int]) -> T:
    ...

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

def random_weighted_item(items: list[T], weight: Callable[[T], int] = lambda i: i.weight) -> T:

הודעת השגיאה היא:

weighted_random_demo.py:44: error: "T" has no attribute "weight"  [attr-defined]
Found 1 error in 1 file (checked 1 source file)

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

  1. לבחור חתימה שעובדת יותר טוב עם ה Type Hints (למשל כמו choices שמקבלת רשימה של פריטים ורשימה של משקלים)

  2. לוותר על ה Type Hints ולהתעקש על החתימה שבחרנו.

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

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

פיתרון Advent Of Code יום 15 בסקאלה - איזה כיף שהמציאו את ListMap

23/03/2024

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

המשך קריאה