ToCode
1.43K subscribers
3.41K links
טיפים קצרים למתכנתים מאת ינון פרק
Download Telegram
import scala.util.chaining._
import scala.collection.immutable.ListMap

object aoc2023day15 {
val demoInput: String = "rn=1,cm-,qp=3,cm=2,qp-,pc=4,ot=9,ab=5,pc-,pc=6,ot=7"

def runningHash(input: String): Int =
val currentValue = 0
input.map(_.toInt).foldLeft(currentValue) {(currentValue, asciiCode) =>
((currentValue + asciiCode) * 17) % 256
}

def focusingPower(boxNumber: Int, index: Int, value: Int): Long =
(boxNumber + 1) * (index + 1) * value

@main
def day15part1(): Unit =
Source
.fromResource("day15.txt")
.getLines()
.next()
.split(',')
.map(runningHash)
.sum
.pipe(println)

@main
def day15part2(): Unit =
Source
.fromResource("day15.txt")
.getLines()
.next()
.split(',')
.foldLeft(Map[Int, ListMap[String, Int]]()) { (boxes, instruction) =>
instruction.split("[-=]") match
case Array(label, value) =>
val boxNumber = runningHash(label)
boxes.updatedWith(boxNumber) {
case Some(lenses) => Some(lenses.updated(label, value.toInt))
case None => Some(ListMap(label -> value.toInt))
}

case Array(label) =>
val boxNumber = runningHash(label)
boxes.updatedWith(boxNumber) {
case Some(lenses) => Some(lenses.removed(label))
case _ => None
}
}
.map { (k, v) => (k, v.zipWithIndex.map {
case ((label, value), index) => focusingPower(k, index, value)
}) }
.values
.map(_.sum)
.sum
.pipe(println)
}
כשאני לא יודע לכתוב Type Hint בפייתון
מנגנון 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 לחתימה מהדוגמה אשמח לשמוע בתגובות או בטלגרם.
👍1
שני תרגילי עוקץ לכבוד פורים
אנחנו ב 2024, וכדאי לזכור שתרגילי עוקץ לא מתו - וזה כולל בתעשיית ההייטק כשאנשים כביכול מודעים לכל הסכנות. הנה שני סיפורים שנתקלתי בהם בתקופה האחרונה ורציתי לשתף כדי להעלות קצת את רמת העירנות של כולנו.

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

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

פה חשדתי.

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

כל הפרטים על העוקץ באתר הרשמי של סקאלה כאן:
https://www.scala-lang.org/blog/2024/03/01/fake-scala-courses.html

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

פה חשדתי.

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

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

אז מסתבר שזה עדיין עובד. אנחנו לא מספיק חשדנים. האינטרנט יכול להיות כלי נפלא אבל תקשורת מהירה עם כל העולם מזמינה גם רמאים.
2👍2
אנחנו ב 2024, ו node.js עדיין מתנהג כאילו זה 2018
נוד הוא כבר מזמן לא סביבת הריצה היחידה בצד שרת להפעלת קוד JavaScript, אבל גם גירסאות עדכניות שלו ממשיכות להציג בעיות שמזמן היו צריכות להיפתר. הנה כמה דוגמאות מהסיבוב האחרון שלי איתו.

תוכניות של קובץ אחד
אנחנו ב 2024 ועדיין אין פיתרון ליצירת סקריפט בקובץ אחד שתלוי בתוכניות אחרות. ב deno אני יכול לכתוב:

import cowsay from 'npm:cowsay@1.6.0';

console.log(cowsay.say({
text : "I'm a moooodule",
e : "oO",
T : "U "
}));


בקובץ אחד ואז להריץ אותו והכל פשוט עובד. ב node צריך להוסיף לתיקיה קובץ package.json ולהכריח את כולם להריץ npm install.

טייפסקריפט
אנחנו ב 2024 ועדיין אני צריך להפעיל tsc בתיקיה לפי שיכול להריץ קבצי TypeScript (כי node מריץ רק JavaScript), אבל אפילו זה לא מספיק כי אם לא תדייק בקבצי ה package.json וה tsconfig.json דברים פשוטים לא יעבדו. שוב להשוואה זה עובד לי ב deno בתור קובץ יחיד בתיקיה:

const res = await fetch('http://api.open-notify.org/astros.json');
const data = await res.json();
console.log(\There are ${data.number} astronauts currently in space\);


אבל ב node אני צריך את שני קבצי ההגדרות ו ts-node בכלל לא מוכן להריץ כזה קובץ גם כש tsc כן מצליח לקמפל אותו.

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

כך עם npx אני כותב פשוט:

$ npx cowsay hello world
_____________
< hello world >
-------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||


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

ב deno אותו משחק נראה ככה:

$ deno run npm:cowsay Hello world


אבל הפלט שונה - במקום להריץ את הסקריפט יש קודם כל רצף של שאלות:

⚠️  Deno requests read access to <CWD>.
├ Requested by \Deno.cwd()\ API.
├ Run again with --allow-read to bypass this prompt.
└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions) >

Granted read access to <CWD>.
⚠️ Deno requests env access to "_".
├ Run again with --allow-env to bypass this prompt.
└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all env permissions) >

Granted env access to "_".
⚠️ Deno requests read access to <main_module>.
├ Requested by \Deno.mainModule\ API.
├ Run again with --allow-read to bypass this prompt.
└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions) >

Granted read access to <CWD>.
Granted env access to "_".
Granted read access to <main_module>.
⚠️ Deno requests env access to "SHELL".
├ Run again with --allow-env to bypass this prompt.
└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all env permissions) >

Granted read access to <CWD>.
Granted env access to "_".
Granted read access to <main_module>.
Granted env access to "SHELL".
⚠️ Deno requests read access to "/Users/ynonp/Library/Caches/deno/npm/registry.npmjs.org/cowsay/1.6.0/package.json".
├ Requested by \Deno.statSync()\ API.
├ Run again with --allow-read to bypass this prompt.
└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions) >

Granted read access to <CWD>.
Granted env access to "_".
Granted read access to <main_module>.
Granted env access to "SHELL".
Granted read access to "/Users/ynonp/Library/Caches/deno/npm/registry.npmjs.org/cowsay/1.6.0/package.json".
⚠️ Deno requests env access to "LC_ALL".
├ Run again with --allow-env to bypass this prompt.
└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all env permissions) >

Granted read access to <CWD>.
Granted env access to "_".
Granted read access to <main_module>.
Granted env access to "SHELL".
Granted read access to "/Users/ynonp/Library/Caches/deno/npm/registry.npmjs.org/cowsay/1.6.0/package.json".
Granted env access to "LC_ALL".
🔥1
⚠️  Deno requests env access to "LC_MESSAGES".
├ Run again with --allow-env to bypass this prompt.
└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all env permissions) >

Granted read access to <CWD>.
Granted env access to "_".
Granted read access to <main_module>.
Granted env access to "SHELL".
Granted read access to "/Users/ynonp/Library/Caches/deno/npm/registry.npmjs.org/cowsay/1.6.0/package.json".
Granted env access to "LC_ALL".
Granted env access to "LC_MESSAGES".
⚠️ Deno requests env access to "LANG".
├ Run again with --allow-env to bypass this prompt.
└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all env permissions) >

Granted read access to <CWD>.
Granted env access to "_".
Granted read access to <main_module>.
Granted env access to "SHELL".
Granted read access to "/Users/ynonp/Library/Caches/deno/npm/registry.npmjs.org/cowsay/1.6.0/package.json".
Granted env access to "LC_ALL".
Granted env access to "LC_MESSAGES".
Granted env access to "LANG".
⚠️ Deno requests env access to "LANGUAGE".
├ Run again with --allow-env to bypass this prompt.
└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all env permissions) >

Granted read access to <CWD>.
Granted env access to "_".
Granted read access to <main_module>.
Granted env access to "SHELL".
Granted read access to "/Users/ynonp/Library/Caches/deno/npm/registry.npmjs.org/cowsay/1.6.0/package.json".
Granted env access to "LC_ALL".
Granted env access to "LC_MESSAGES".
Granted env access to "LANG".
Granted env access to "LANGUAGE".
⚠️ Deno requests read access to "/Users/ynonp/Library/Caches/deno/npm/registry.npmjs.org/cowsay/1.6.0/cows/default.cow".
├ Requested by \Deno.readFileSync()\ API.
├ Run again with --allow-read to bypass this prompt.
└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions) >


Granted read access to <CWD>.
Granted env access to "_".
Granted read access to <main_module>.
Granted env access to "SHELL".
Granted read access to "/Users/ynonp/Library/Caches/deno/npm/registry.npmjs.org/cowsay/1.6.0/package.json".
Granted env access to "LC_ALL".
Granted env access to "LC_MESSAGES".
Granted env access to "LANG".
Granted env access to "LANGUAGE".
Granted read access to "/Users/ynonp/Library/Caches/deno/npm/registry.npmjs.org/cowsay/1.6.0/cows/default.cow".
_____________
< Hello world >
-------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||



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

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

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

╭───┬────────────────────────┬───────────╮
│ # │ name │ size │
├───┼────────────────────────┼───────────┤
│ 0 │ 03-one │ 64 B │
│ 1 │ 04-two │ 64 B │
│ 2 │ 05-three │ 64 B │
│ 3 │ career-craft-ai-bg.jpg │ 225.3 KiB │
│ 4 │ clone.nu │ 153 B │
│ 5 │ inc.nu │ 229 B │
│ 6 │ text.txt │ 82 B │
│ 7 │ wiki.nu │ 239 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
}
נו זה מתחיל פשוט עם לולאת 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 נראה כמו פרויקט מבטיח - הוא עובד מהר ועבודה עם מידע במקום עם טקסט יכולה לחסוך טעויות בתוכניות מורכבות.
כשהתקן עובד לרעתך
בגירסאות ישנות של הספריה הסטנדרטית של דינו היה מודול בשם 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}\);
});



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

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

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. גרמלין נותן הכי הרבה שליטה באיך מבוצעת השאילתה, ומאפשר מאוד בקלות להתאים את אופן סריקת הגרף למודל הנתונים הספציפי של המערכת.
קריאה וכתיבה לקבצים עם 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);
בהעתקת קבצים אני מעדיף לוותר על ה Text Decoding כי בצורה כזאת אני מעתיק את הביטים כמו שהם מקובץ המקור בלי קשר אם הם מייצגים טקסט או לא או מה הקידוד שלהם.

למידע נוסף על Streams וכל מה שאפשר לעשות איתם ב Node.JS שווה להעיף מבט בתיעוד:

https://nodejs.org/api/webstreams.html

כל הדוגמאות בפוסט הזה נבדקו ועובדות ב Node גירסה 21.7 אחרי ששמרתי את הקוד בקובץ בשם a.mjs.