ToCode
Kanalga Telegram’da o‘tish
טיפים קצרים למתכנתים מאת ינון פרק
Ko'proq ko'rsatish1 422
Obunachilar
+224 soatlar
+37 kunlar
+630 kunlar
Postlar arxiv
1 423
טרנזאקציות
שליחת הפוסט אתמול לטלגרם עבדה חלקית, וזה מעניין. ביום רגיל יש תוכנית פייתון ששוברת את הפוסט להודעות שייכנסו בטלגרם (יש להם מגבלה של מילים) וגם מוסיפה Escape Characters איפה שצריך. בגלל שטלגרם נודניקים לא תמיד הקוד שלי מוסיף את ה Escape Characters במקומות הנכונים ולכן שליחה של הודעות לפעמים נכשלת.
ומה קורה כשיש פוסט ארוך ובעיה בהוספת ה Escape Characters בהודעה השנייה? השלישית? אולי הודעה באמצע? אתם כבר מבינים לאן זה הולך, הפרסום לטלגרם יישלח חלקית ונקבל פוסט חתוך.
כשהקוד טוב בעיות כאלה קורות מעט מאוד, אבל כשהן קורות קשה להתמודד איתן. בואו נחשוב על הטלגרם ופיתרונות אפשריים לאתגר:
1. צעד ראשון יהיה ליצור קבוצת "בדיקה", לשלוח אליה את כל החלקים של הפוסט ורק אם הכל עבר לשלוח את אותם חלקים לקבוצה האמיתית. אבל זה לא מספיק טוב כי אולי יש משהו שונה בהגדרות בין קבוצת הבדיקה לקבוצה האמיתית שיגרום להודעות לעבור לקבוצת הבדיקה ולהיכשל בשליחה לקבוצה האמיתית.
2. פיתרון קצת יותר טוב יהיה בדיקה אחרי שליחת כל הודעה לראות אם השליחה נכשלה, ואם כן למחוק את כל ההודעות ברצף. גם זה לא מספיק טוב כי אולי התוכנית עצמה התרסקה ולכן אין מי שימחק את הרצף.
3. פיתרון עוד קצת יותר טוב יהיה להריץ את תוכנית השליחה כל חמש דקות במקום כל פעם ביום. התוכנית תבדוק אם הפוסט המלא נשלח לטלגרם. אם כלום לא נשלח היא תנסה לשלוח, ואם היא מוצאת חלק מהפוסט בקבוצה היא תמחק את מה שמצאה.
4. בשביל לא להיכנס ללופ אותה תוכנית שרצה פעם בחמש דקות תצטרך לסמן באיזשהו בסיס נתונים ששליחה נכשלה ושהיא מחקה את כל השאריות (או שהשליחה הצליחה). כשהתוכנית עולה היא תוכל לבדוק בבסיס הנתונים באיזה סטטוס היא סיימה פעם קודמת וכך היא תדע אם דרושה עוד עבודה.
פיתוח תוכנה, כל תוכנה, דורש איזון בין הרצון שלנו לבנות את הקוד הכי אמין וטוב שאפשר לבין המציאות והמשאבים שיש לנו (כולל משאבים של תחזוקה ובאגים בקוד יותר מורכב). במקרה של פירסום לטלגרם הייתי עוצר בסעיף 2 כדי לקבל קוד טוב יותר מהקוד שיש לי היום שם, אבל עדיין לא ידרוש יותר משאבים.
1 423
טרנזאקציות
שליחת הפוסט אתמול לטלגרם עבדה חלקית, וזה מעניין. ביום רגיל יש תוכנית פייתון ששוברת את הפוסט להודעות שייכנסו בטלגרם (יש להם מגבלה של מילים) וגם מוסיפה Escape Characters איפה שצריך. בגלל שטלגרם נודניקים לא תמיד הקוד שלי מוסיף את ה Escape Characters במקומות הנכונים ולכן שליחה של הודעות לפעמים נכשלת.
ומה קורה כשיש פוסט ארוך ובעיה בהוספת ה Escape Characters בהודעה השנייה? השלישית? אולי הודעה באמצע? אתם כבר מבינים לאן זה הולך, הפרסום לטלגרם יישלח חלקית ונקבל פוסט חתוך.
כשהקוד טוב בעיות כאלה קורות מעט מאוד, אבל כשהן קורות קשה להתמודד איתן. בואו נחשוב על הטלגרם ופיתרונות אפשריים לאתגר:
1. צעד ראשון יהיה ליצור קבוצת "בדיקה", לשלוח אליה את כל החלקים של הפוסט ורק אם הכל עבר לשלוח את אותם חלקים לקבוצה האמיתית. אבל זה לא מספיק טוב כי אולי יש משהו שונה בהגדרות בין קבוצת הבדיקה לקבוצה האמיתית שיגרום להודעות לעבור לקבוצת הבדיקה ולהיכשל בשליחה לקבוצה האמיתית.
2. פיתרון קצת יותר טוב יהיה בדיקה אחרי שליחת כל הודעה לראות אם השליחה נכשלה, ואם כן למחוק את כל ההודעות ברצף. גם זה לא מספיק טוב כי אולי התוכנית עצמה התרסקה ולכן אין מי שימחק את הרצף.
3. פיתרון עוד קצת יותר טוב יהיה להריץ את תוכנית השליחה כל חמש דקות במקום כל פעם ביום. התוכנית תבדוק אם הפוסט המלא נשלח לטלגרם. אם כלום לא נשלח היא תנסה לשלוח, ואם היא מוצאת חלק מהפוסט בקבוצה היא תמחק את מה שמצאה.
4. בשביל לא להיכנס ללופ אותה תוכנית שרצה פעם בחמש דקות תצטרך לסמן באיזשהו בסיס נתונים ששליחה נכשלה ושהיא מחקה את כל השאריות (או שהשליחה הצליחה). כשהתוכנית עולה היא תוכל לבדוק בבסיס הנתונים באיזה סטטוס היא סיימה פעם קודמת וכך היא תדע אם דרושה עוד עבודה.
פיתוח תוכנה, כל תוכנה, דורש איזון בין הרצון שלנו לבנות את הקוד הכי אמין וטוב שאפשר לבין המציאות והמשאבים שיש לנו (כולל משאבים של תחזוקה ובאגים בקוד יותר מורכב). במקרה של פירסום לטלגרם הייתי עוצר בסעיף 2 כדי לקבל קוד טוב יותר מהקוד שיש לי היום שם, אבל עדיין לא ידרוש יותר משאבים.
1 423
יום 17 של Advent Of Code 2023 - היום בו סקאלה עבדה נגדי
אני לא יודע אם זה הקוד שלי או משהו מובנה בשפה, אבל התרגיל של יום 17 היה הכי מאתגר עד כה. בפעם הראשונה בסידרה הרגשתי שמבני הנתונים ה Immutable של סקאלה שכל כך כיף לעבוד איתם לא מצליחים לספק את הביצועים הנדרשים ועברתי להשתמש ב Mutable Collections. בואו נראה את התרגיל והפיתרון וכמו תמיד תרגישו בנוח להציע פיתרונות טובים יותר או יעילים יותר למקרה שפספסתי משהו.
האתגר - חיפוש מסלול קצר ביותר בגרף
האתגר של יום 17 הוא מימוש כמעט קלאסי של אלגוריתם מציאת המסלול הקצר ביותר בגרף. המסלול שלנו בנוי בצורת גריד של מספרים לדוגמה:
2413432311323
3215453535623
3255245654254
3446585845452
4546657867536
1438598798454
4457876987766
3637877979653
4654967986887
4564679986453
1224686865563
2546548887735
4322674655533
אנחנו מתחילים מהנקודה השמאלית עליונה וצריכים להגיע לפינה הימנית תחתונה של הלוח, ולמצוא את המסלול שסכום המספרים עליו הוא הנמוך ביותר (לא סופרים את ה-2 של נקודת ההתחלה, אלא אם כן נעבור בה פעם שנייה). יש גם טוויסט כי אסור שיהיו במסלול יותר מ-3 צעדים באותו כיוון.
בשביל למצוא מסלול קצר ביותר אנחנו יוצאים מנקודת ההתחלה ומסמנים את כל הנקודות אליהן אפשר להגיע, ולכל נקודה שומרים מה ה"ניקוד" של אותה נקודה. שמים את נקודת ההתחלה בצד ומכל הנקודות שנשארו מוצאים את זו עם הניקוד הנמוך ביותר וממשיכים ממנה את המסלול כדי לקבל את הנקודות אליהן אפשר להגיע מנקודה זו, ואז גם את הנקודה השנייה שמים בצד ושוב ומוצאים את הנקודה עם הניקוד הכי נמוך וממשיכים ממנה את המסלול. כך ממשיכים עד שמגיעים לסוף.
הניקוד בתרגיל הזה מורכב מסכום של שני דברים: המרחק מהיעד וסכום המספרים שהוביל אותנו לנקודה. כשלוקחים כל פעם את הנקודה עם הניקוד הכי נמוך וממשיכים ממנה אנחנו מבטיחים שנתקדם בכיוון הנכון ובמסלול עם הסכום הנמוך ביותר.
פיתרון בסקאלה
לא משנה כמה ניסיתי לכתוב את רקורסיית הזנב בצורה יעילה, בסופו של דבר חיפוש הנקודה הבאה מתוך אוסף כל הנקודות הפוטנציאליות לקח יותר מדי זמן - ורק גדל ככל שאספתי יותר אפשרויות. מה שהייתי צריך כאן היה scala.collection.mutable.PriorityQueue שהוא חלק ממבני הנתונים ה Mutable של סקאלה.
נקודה נוספת חשובה מהפיתרון היא ההתמודדות עם המגבלה של שלושה צעדים מקסימליים לאותו כיוון. בשביל זה הגדרתי לכל צומת גם את הכיוון בו הגעתי אליו, וכך התיחסתי לאותו צומת בצורה שונה אם הגעתי אליו בצורה אנכית או אופקית.
איך עובד הקוד? קודם כל מגדירים את הצומת עם הפונקציה שמחזירה את כל השכנים:
case class Node(pos: (Int, Int),
direction: Direction,
count: Int,
map: Map[(Int, Int), Int]) {
def move(newPosition: (Int, Int), newDirection: Direction): Option[Node] =
if (((count == 3) && newDirection == direction) || (!map.contains(newPosition))) {
None
} else {
Some(Node(
pos=newPosition,
direction=newDirection,
count=if ((newDirection == direction) || (direction == Direction.Start)) count + 1 else 1,
map=map))
}
def movePart2(newPosition: (Int, Int), newDirection: Direction): Option[Node] =
if (!map.contains(newPosition)) {
return None
}
if ((newDirection == direction) && (count >= 10)) {
return None
}
if ((newDirection != direction) && (direction != Direction.Start) && (count < 4)) {
return None
}
Some(Node(
pos = newPosition,
direction = newDirection,
count = if ((newDirection == direction) || (direction == Direction.Start)) count + 1 else 1,
map = map))
def up: Option[Node] = if (direction == Direction.Down) {
None
} else {
move(newPosition=(pos._1 - 1, pos._2), newDirection=Direction.Up)
}
def down: Option[Node] = if (direction == Direction.Up) {
None
} else {
move(newPosition=(pos._1 + 1, pos._2), newDirection=Direction.Down)
}
def left: Option[Node] = if (direction == Direction.Right) {
None
} else {
move(newPosition=(pos._1, pos._2 - 1), newDirection=Direction.Left)
}1 423
נשים לב ש esbuild לא בודק הגדרות טיפוסים ולכן בנוסף אליו נצטרך להגדיר קובץ tsconfig.json לפרויקט כדי להריץ את ה TypeScript Compiler, תוך שאנחנו זוכרים ש tsc יבנה רק גירסה של הפרויקט שמותאמת ל node (וכן זה היה מושלם אם היתה דרך לשכנע את tsc לבנות גירסה שמתאימה ל deno. עד כמה שאני יודע אין).
בואו נראה את זה בקוד.
בתיקיית הדוגמה אני משנה את הסיומות של שני קבצי הקוד ל ts, מוסיף קובץ tsconfig.json עם התוכן הבא:
{
"compilerOptions": {
"target": "ESNext",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"strict": true,
"skipLibCheck": true,
"outDir": "./distNode"
}
}
משנה את שורת ה import ל:
import { text } from './helper';
ומריץ tsc כדי לראות שהכל מתקמפל.
אחרי זה אני מריץ את שתי פקודות ה esbuild שהראיתי:
$ esbuild --bundle main.ts --packages=external --outdir=distNode --format=cjs
$ esbuild --bundle main.ts --packages=external --outdir=distDeno --format=esm
ונוצרו לי שני פרויקטים בשתי תיקיות היעד. אני יכול להריץ כל אחד מהם עם הכלי שמתאים לו:
$ deno run -A distDeno/main.js
_________________
< I'm a moooodule >
-----------------
\ ^__^
\ (oO)\_______
(__)\ )\/\
U ||----w |
|| ||
$ node distNode/main.js
_________________
< I'm a moooodule >
-----------------
\ ^__^
\ (oO)\_______
(__)\ )\/\
U ||----w |
|| ||
בשביל לבדוק שהכל מתקמפל גם עם דינו נוכל להפעיל:
$ deno check --unstable-sloppy-imports *.ts
כאשר המתג --unstable-sloppy-imports גורם לדינו לעבוד גם עם import-ים ללא סיומת, כמו אלה בקבצי המקור של הטייפסקריפט שלנו. אבל זה מתג עבודה לא מומלץ ואמור לרדת בגירסה 2 של דינו.
בעיניי שיטה זו פחות טובה גם בגלל שהיא מסורבלת יותר וגם בגלל שאין באמת טעם לבנות את אותו פרויקט גם ל node וגם ל deno.
כיוון עבודה מומלץ
אנחנו נמצאים היום בצומת דרכים מבחינת העתיד של סביבות להרצת JavaScript מחוץ לדפדפן. עד לא מזמן node.js היתה האופציה היחידה שלנו, אבל היום יש כבר שלוש סביבות טובות להרצה - deno ,node, ו bun שהוא ניסיון לייצר סביבה אפילו יותר מהירה מ deno ויותר תואמת ל node.js. אני לא יודע איך התחרות הזאת תיגמר, ואם בסופו של דבר יהיה מנצח או שאנשים ישתמשו בשלושת הכלים לפי סוג הפרויקט.
כרגע יש חבילות שעובדות טוב יותר בדינו, וחבילות אחרות שעובדות טוב יותר ב node, וחבילות שעובדות די טוב בשתי הסביבות. אקספרס עובדת סביר בשתי הסביבות אבל עדיין פיצ'רים מסוימים שלה עובדים רק ב node. לדינו יש פריימוורק מתחרה וספציפי לדינו בשם Oak. נקסט עובדת רק ב node אבל לדינו יש פריימוורק מתחרה בשם fresh.
עוד דוגמה היא חבילות החיבור לבסיס נתונים. דרייברים של בסיס נתונים לא עובדים בצורה חלקה בין שתי הסביבות ויש הרבה ספריות לגישה לבסיס נתונים שמוגבלות רק ל node.js. אני אוהב לעבוד עם ספריה בשם kysely שכן עובדת על שתי הסביבות (יש גם את drizzle שעובדת בכל מקום), אבל הרבה ספריות מובילות במיוחד ספריות ORM לא תומכות בדינו.
אני מקווה לראות את האקוסיסטם של דינו ממשיך לגדול ויום אחד לקבל מערכת שמכילה מספיק מודולים ותיעוד כדי שאפשר יהיה לעבוד איתה בקלות בלי קשר ל node.js. כרגע דינו מתאים יותר להרפתקנים או לפיתוח יישומים ספציפיים. שירות Deno Deploy שלהם הוא מצוין ולכן כן הייתי בונה איתו היום מערכת ווב תוך שימוש ב Web Frameworks שלהם, אבל החיבור למודולים מ npm עדיין לא עובד בצורה מושלמת ובעיות תאימות שם יכולות לתסכל.1 423
פיתוח קוד שמתאים גם ל Node וגם ל Deno
מצד אחד deno הולך להיות הדבר הגדול הבא אבל מצד שני הוא עדיין לא שם. האם אפשר לשלב את דינו בקטנה? האם אפשר לכתוב קוד node.js שיעבוד בעתיד גם ב deno כשנחליט לשדרג? ומה העלות? בואו ננסה כמה דוגמאות.
הבדלים בין Deno ל Node
נתחיל בדברים הפשוטים - בפרויקט JavaScript רגיל (ללא טייפסקריפט), אם נגדיר את התלויות שלנו בקובץ deno.json ובקובץ package.json במקביל נוכל לכתוב קוד שיעבוד בשתי הסביבות, וזו תהיה הדוגמה הראשונה לפוסט זה:
קובץ package.json:
{
"type": "module",
"dependencies": {
"cowsay": "1.6.0"
}
}
קובץ deno.json:
{
"tasks": {
"dev": "deno run --watch main.ts"
},
"imports": {
"cowsay": "npm:cowsay@^1.6.0"
}
}
קובץ main.js:
import cowsay from 'cowsay';
import { text } from './helper.js';
console.log(cowsay.say({
text : text(),
e : "oO",
T : "U "
}));
קובץ helper.js:
export function text() {
return "I'm a moooodule";
}
הבעיה מתחילה במעבר לטייפסקריפט, ובעיקר בגלל הקשר בין מודולים ביישום. נתבונן בשורה:
import { text } from './helper.js';
גם node וגם deno יודעים להתמודד עם כתיב ES Modules ולטעון סמלים מקבצי JavaScript אחרים. הבעיה היא ש node לא יודע לקרוא קבצי טייפסקריפט ישירות ולכן במעבר לטייפסקריפט ב Node צריך להפעיל כלי נוסף - ה TypeScript Compiler.
הקומפיילר של טייפסקריפט כרגע לא יודע להתמודד עם שורות import שמייבאות קבצי ts אחרים, כלומר זה לא עובר קומפילציה:
import { text } from './helper.ts;
בטייפסקריפט עלינו להשמיט את הסיומת והקומפיילר של טייפסקריפט יוסיף אותה בעצמו לפי הדבר שהוא בונה. בשביל ששורה כמו שהראיתי תתקמפל בטייפסקריפט אני צריך לכתוב:
import { text } from './helper;
אבל Deno לא מוכן לטעון מודול טייפסקריפט אם אין סיומת ts מפורשת לקובץ. הוא מצפה שמודול טייפסקריפט ייטען בדיוק כמו מודול JavaScript וכמו שמודולים נטענים בדפדפן - כלומר עם הסיומת. (סוגריים - יש אופציה לעקוף את המגבלה הזאת אבל הם רוצים להוריד אותה וממליצים לא להשתמש בה, אז אני מעדיף להשאיר אותה מחוץ לשיחה).
הבדל נוסף שהולך להפריע בהרבה תוכניות הוא שבשביל להתיחס לתיקיה או הקובץ הנוכחי ב node יש משתנים מיוחדים בשם __dirname ו __filename. לדינו אין אותם והוא משתמש במשתנה הסטנדרטי import.meta.url. נוד מכיר את import.meta.url רק במצב עבודה עם ES Modules, אבל במצב זה הרבה מודולים מ npm לא עובדים במיוחד מתוך טייפסקריפט.
פיתרון 1 - בוחרים סביבה
אני חייב להודות שהפיתרון הכי טוב לבעיית התאימות וההמלצה שלי היא לבחור את אחת הסביבות ולהישאר איתה. אם שוכנעתם שהעתיד הוא node.js וטייפסקריפט תבנו את היישום בזה ואם אתם חושבים שדינו הולך להשתלט על העולם לכו עליו. לפעמים גם יש אילוצים שלא תלויים בנו, למשל ספריית next.js לא תומכת בדינו ולכן אם אתם רוצים לכתוב יישום next.js תצטרכו לבחור ב Node. הבחירה בכלי אחד גם מאפשרת לכם להשתמש ביכולות הספציפיות של הכלי (בין אם זה node או Deno) וכך לכתוב קוד יעיל יותר.
אם יום אחד תצטרכו לעבור לסביבה השניה לפחות תדעו במה זה כרוך ותוכלו להעריך את ההשקעה הדרושה.
פיתרון 2 - בניה עם esbuild, הרצה איך שרוצים
אם בכל זאת מתעקשים לכתוב קוד שיהיה כמו שיותר תואם לשתי הסביבות אפשר לקמפל את הפרויקט עם כלי שמחבר אוטומטית את כל קבצי המקור לקובץ אחד וכך אין import בין קבצים שונים שלכם והכל מסתדר עם הסיומות. אני יודע זה נשמע רעיון מוזר אבל הוא עובד. כלי אחד כזה נקרא esbuild. הוא מהיר וקל לעבוד איתו דרך שורת הפקודה.
נתקין את esbuild עם:
npm install -g esbuild
ועכשיו הפקודה:
$ esbuild --bundle main.ts --packages=external --outdir=distNode --format=cjs
בונה את הפרויקט לתיקיה בשם distNode בצורה שמתאימה ל node, והפקודה:
$ esbuild --bundle main.ts --packages=external --outdir=distDeno --format=esm
בונה את הפרויקט לתיקיית distDeno בצורה שתואמת לדינו. הפרויקט ייבנה תמיד לקובץ js אחד ולכן לא מבצע import-ים יחסיים בין קבצי מקור והכל עובד.1 423
לא נראה כמו Ajax
הביטוי Ajax הוא בכלל קיצור של Asynchronous JavaScript and XML. עכשיו אפשר לשאול, מה קשור XML? אנחנו ב 2024, אבל אני כותב את זה בשביל להזכיר איך דברים משתנים כל הזמן. ה Ajax קשור ל XML כי כשדפדפנים רק התחילו לפנות לשרתים אחרי שעמוד HTML נטען הם השתמשו בממשק שנקרא
XMLHttpRequest, שבכלל היה חלק מחבילה של XML (אפילו שדרך הממשק הזה הם קיבלו את התשובה ב JSON). לימים כולם עברו להשתמש בממשק fetch שבכלל לא כולל את המילה XML בשביל לבנות את אותם מנגנונים.
עם גירסה 14 של next.js הקונספט של Ajax שוב משתנה. הפעם מספיק להפעיל await מתוך קוד טיפול באירוע כדי לשלוח הודעה לשרת, לקבל תשובה ולפענח אותה. שימו לב לקוד הבא בצד הלקוח:
async function handleInput(ev: FormEvent<HTMLInputElement>) {
if (ev.target) {
const input = ev.target as HTMLInputElement;
const text = input.value;
const options = await search(text);
setOptions(options);
}
}
ולקוד שמתאים לו בצד השרת:
export async function search(what: string) {
if (what.length < 3) {
return []
} else {
return sentences.filter(s => s.toLowerCase().includes(what))
}
}
יש פה כמה דברים מדהימים:
1. אין בשום מקום הגדרה של Endpoint. כל החיבור בין הלקוח לשרת ויצירת ה REST API קרה אוטומטית. כל כך התרגלנו שאנחנו צריכים לחשוב על ממשקים ששכחנו שהרבה פעמים אנחנו בונים את הממשק רק בשביל אפליקציה אחת, ויהיה יותר קל לעבוד עם ממשק שבונה את עצמו.
2. אין צורך לכתוב את הקריאה ל fetch. לא צריך לבנות RTK Query או React Query או שום דבר. הפרמטרים עוברים כמו העברת פרמטרים רגילה ב JavaScript.
האם זה העתיד? לאפליקציות מסוימות בהחלט כן. הבעיה היחידה שעדיין נשארה היא שאין ל next מנגנון טוב לעדכן React Server Components אחרי שינויים בשרת, מה שאומר שאנחנו עדיין צריכים לשמור Client Side State בהרבה אפליקציות. אני מקווה בעתיד הקרוב לראות גם את זה נפתר עם מנגנון Subscriptions אוטומטי ואז נוכל רשמית להיפרד מ Redux וחבריו.
נ.ב. רוצים לראות את הקוד הזה בפעולה? זה הלינק:
https://next-search-demo.vercel.app/
נסו לכתוב בתיבה java, כדי לראות את אחד המשפטים. יש 15. כשיימאס לכם לנחש תוכלו לראות את כולם יחד עם קוד המערכת בגיטהאב כאן:
https://github.com/ynonp/next-search-demo1 423
טיפ JavaScript - בואו נסתיר מאפיין מ Object.keys
הפונקציה
Object.keys מחזירה את כל המפתחות באובייקט נכון? לא בדיוק. זאת ההגדרה שלה ב MDN:
> The Object.keys() static method returns an array of a given object's own enumerable string-keyed property names.
מילת המפתח כאן היא enumerable. היא גורמת ל Keys להחזיר רק את המפתחות שיחזרו מ for ... in, או ליתר דיוק רק את אלה מתוך for ... in שלא הגיעו מהפרוטוטייפ.
הפעלה רגילה עשויה לתת את ההרגשה שזה כל המפתחות:
const object1 = {
a: 'somestring',
b: 42,
c: false,
};
console.log(Object.keys(object1));
// Expected output: Array ["a", "b", "c"]
אבל זה לא מדויק. ב JavaScript אני יכול להגדיר גם מאפיינים שלא יופיעו ברשימה הזאת במספר דרכים. דרך אחת היא הפונקציה defineProperty. שימו לב לקוד הבא:
const object1 = {
a: 'somestring',
b: 42,
c: false,
};
Object.defineProperty(object1, 'secret', {
value: 'secret',
enumerable: false
})
console.log(Object.keys(object1));
console.log(object1.secret)
הפעם אנחנו עדיין מקבלים רק את המפתחות a, b ו c, למרות שההדפסה השנייה מצליחה ומדפיסה את המילה secret.
ואיך בכל זאת נקבל את כל רשימת המפתחות כולל אלה שאינם enumerable? ב JavaScript חשבו על הכל ויש פונקציה אחרת בשם getOwnPropertyNames שמחזירה את כל המפתחות. הקריאה הזו:
console.log(Object.getOwnPropertyNames(object1))
מדפיסה את:
Array ["a", "b", "c", "secret"]1 423
פונקציית Pipe ושרשור מתודות
בתיעוד של אמזון אנחנו מוצאים את הדוגמה הבאה לשימוש ב Polly ב Java:
new SynthesizeSpeechRequest()
.withText(text)
.withVoiceId(voice.getId())
.withOutputFormat(format).withEngine("neural");
התבנית הזאת נקראת Builder והיא מציעה טכניקה להתמודד עם בנאי שצריך לקבל הרבה פרמטרים. הרעיון הוא שבמקום להעביר את כל הפרמטרים בקריאה אחת בבנאי אנחנו נפעיל עוד ועוד פונקציות על האוביקט כשכל פונקציה מגדירה עוד פרמטר לבניית הדבר שאנחנו רוצים לבנות. הייתרון בתבנית ה Builder הוא שאפשר לבנות את הדבר בשלבים ואפילו לשלב באמצע תנאים או לולאות.
תבנית דומה לה נקראת Fluent Interface והיא מציעה שימוש בשרשור מתודות כדי לתאר פונקציונאליות או רצף פעולות. לדוגמה הקוד הבא מספריית jQuery:
$('#myButton')
.click(function() {
$(this).addClass('active');
})
.hover(
function() {
$(this).css('background-color', 'lightblue');
},
function() {
$(this).css('background-color', '');
}
)
.fadeOut(1000)
.fadeIn(1000);
התבנית מתארת ממשק בצורה נוחה של קריאות בשרשרת לפונקציות השונות של האוביקט. כמו ב Builder, גם ב Fluent Interface כל פונקציה מחזירה את האוביקט שעליו אנחנו עובדים וכך אפשר לחבר עוד ועוד פעולות.
אבל הבעיה בתבנית הזאת ובכל שרשור של פונקציות היא שקשה לראות איך לחבר את זה לתנאים ולולאות שאנחנו מכירים. בדוגמה של ה jQuery אם הייתי רוצה להפעיל פעולה 10 פעמים ברצף הייתי צריך לכתוב אותה ממש 10 פעמים, או לשמור את מצב הביניים של השרשרת למשתנה כדי שאוכל להמשיך את השרשרת על המשתנה בתוך הלולאה.
טכניקה פשוטה להתמודד עם לולאות בתוך שרשראות של פונקציות היא הפונקציה tap. היא קיימת בהמון שפות ובשמות שונים ובכל מקרה אפשר תמיד לממש אותה ממש בקלות, כשהרעיון הבסיסי הוא ש tap היא מתודה שיש לכל אוביקט בשפה, היא מקבלת בתור פרמטר פונקציה כלשהי, היא תפעיל את הפונקציה ותחזיר את האוביקט (ה this). מימוש פשוט ב JavaScript של tap נראה כך:
function tap(fn) {
fn(this);
return this;
};
בואו ניקח דוגמה מ Ruby שם tap כבר מובנית בשפה ונראה איך להשתמש בה כדי להוסיף לולאות לשרשראות של פונקציות. אני מתחיל עם מחלקה בשם Polly שעובדת בתבנית הבנאי עם הקוד הבא:
class Polly
attr_accessor :text, :engine
def initialize
@text = []
end
def with_text(text)
@text.append(text)
self
end
def with_engine(engine)
@engine = engine
self
end
def print
puts "Engine: #{@engine}; Text: #{@text}"
end
end
ועכשיו אני רוצה להפעיל את with_text בלולאה עם המחרוזות a, b ו c. אפשר כמובן להשתמש במשתנה ואז נקבל:
p = Polly.new
p.with_engine("engine")
['a', 'b', 'c'].each {|t| p.with_text(t) }
p.print
אבל אם רוצים לוותר על המשתנה אפשר להשתמש ב tap ואז נקבל:
Polly
.new
.with_engine("engine")
.tap { |p| ['a', 'b', 'c'].reduce(p, &:with_text) }
.print1 423
היום למדתי (שוב) - תמיד לסמן שגיאות
יש פה באתר מנגנון שמאפשר לכם לקבל כל פוסט חדש מהבלוג לאימייל. אבל אם נרשמתם ומכל מיני סיבות לא הצלחתי לשלוח לכם את המייל אני מבטל את הרישום כדי לא לשלוח סתם. עד אתמול זה היה הקוד שהיה אחראי על המנגנון:
def bounced
mp = find_mp('bounced')
if mp.present?
mp.update(sent_status: :failed)
mp.prospect.subscriptions.destroy_all
end
head :ok
end
בגדול המסלול התקין מופיע בפונקציה בצורה מאוד ברורה - אם קיבלנו הודעה שאי אפשר היה לשלוח את המייל אז נמחק את המנוי כדי שלא נצטרך לשלוח מיילים גם מחר. מסלול השגיאות זו כבר בעיה אחרת. הפונקציה נכתבה כדי להצליח תמיד, כי ההודעה מגיעה ב Webhook ולא אכפת לשרת המיילים ששלח את ההודעה אם מצאתי או לא מצאתי את המנוי עליו הוא מדווח.
אבל לי זה אכפת.
כי אם הם משנים את שם האירוע - במקרה שלנו זה השתנה מ bounced ל failed, אז החיפוש תמיד ייכשל אבל הכל יראה תקין, אפילו שהמערכת תתעלם מכל ההודעות על כשלונות. זה פשוט יראה כאילו כל שליחת מייל מצליחה.
הפיתרון הוא קל אבל האתגר לטווח הארוך הוא קשה: צריך לזכור תמיד שדברים יכולים להשתנות, וגם כשאנחנו מוכנים "להכיל" כשלונות עדיין לרשום אותם ולדווח עליהם. המערכת לא צריכה להתרסק ולא לגרום לתגובת שרשרת כשדברים רעים קורים, אבל כן כדאי לדווח על זה כדי שאפשר יהיה לתקן בזמן.1 423
חמש בעיות מרכזיות שיש לי עם דינו היום
דינו הוא ההבטחה הגדולה הבאה אבל בינתיים ולמרות שהם כל הזמן נראים בכיוון הנכון יש עדיין כמה אתגרים משמעותיים למי שינסה לאמץ אותו ובמיוחד אם רוצים לשלב עבודה עם קוד ישן. אלה הבעיות המרכזיות שלי עם דינו היום -
1. מאגר חבילות - דינו תומכים ב JSR, ב NPM ובטעינה של כל קובץ חבילה מ denoland. אבל
deno add יודע לעבוד רק עם חבילות npm ו jsr, ואי אפשר לשנות את ברירת המחדל שלו. זה מתיש. אני מבין שהחלום שלהם הוא שכל החבילות יעבדו ב JSR אבל עד שזה יקרה צריכים לראות שאפשר לעבוד עם npm בצורה הרבה יותר חלקה.
2. באגים מוזרים בחבילות מ npm - הוספתם תמיכה ב npm וזה מעולה, אבל צריך גם לוודא שהקוד משם רץ או לפחות ליצור רשימה מסודרת של דברים שידוע שלא עובדים. בניסיון שלי להעביר קוד מאקספרס לדינו גיליתי לגמרי במקרה ש express.static לא עובד וגם cookie-session. איזה עוד? ואיך זה יתנהג על מערכות הפעלה שונות? אלה דברים שכל פרויקט פורטינג יצטרך לגלות לבד ואפילו לא בתחילת הפרויקט.
3. חסרות חבילות במיוחד דרייברים של בסיסי נתונים - הדרייבר של SQLite לא עובד על דינו ויש חבילה אחרת עם דרייבר אחר. על MSSql אין בכלל מה לדבר. קיטור שמצאתי ברדיט ומאוד התחברתי אליו אמר:
> I'm spending wayyyy too much time on this. I really wish someone could plug up this one hole in the Deno libraries -- it's the only thing stopping me from getting my company to let me convert everything to Deno (which I desperately want to do).
4. גירסה 0.2 של החבילה הסטנדרטית - אני יודע יש שיגידו שאני נטפל לשטויות ומה זה מספר גירסה אבל אם עדיין לא הצלחתם להגיע לפחות לגירסה 1 של החבילה הסטנדרטית מה זה אומר? הרי דינו עצמו תכף מגיע לגירסה 2.
5. יש אפשרות לטעון מודולים מובנים ב node עם התחילית node:. רובם עובדים אבל גם כאן התאימות לא 100%. לפחות פה הם פירסמו טבלת תאימות.
סך הכל דינו נראה כמו הדור הבא של node.js. חבל רק שההתעקשות שלהם על הדרך החדשה והנכונה לעשות דברים באה על חשבון נוחות של המשתמשים. המסע לאימוץ דינו הולך להיות ארוך וכנראה יחייב פרידה מספריות ישנות ומעבר לחדשות. זה אפשרי אבל זה לא יקרה מחר בבוקר ובינתיים עדיין קשה לראות את המוטיבציה של אנשים להחליף במיוחד כל עוד node.js ממשיך להיות מתוחזק.1 423
בואו נכתוב את maxBy ב TypeScript
הפונקציה
maxBy היתה יכולה להיות יופי של תוספת ל JavaScript ו TypeScript אבל מכל מיני סיבות לא נכללה בסטנדרט. בואו נראה איך לתקן את הבעיה עם reduce בצורה ידידותית ל TypeScript.
חתימה
אני רוצה לבנות פונקציה בשם maxBy שתקבל מערך ופונקציית מפתח ותחזיר את האיבר מהמערך עבורו פונקציית המפתח היא הגבוהה ביותר. החתימה היא לכן:
function maxBy<T>(data: Array<T>, key: (t: T) => number): T { ... }
אני מדמיין שאני מפעיל אותה באופן הבא:
const data = [
{ name: "Leela", age: 31 },
{ name: "Fry", age: 1031 },
{ name: "Hubert", age: 165 },
{ name: "Bender", age: 10 }
];
console.log(maxBy(data, d => d.age));
ומקבל את Fry שגילו 1031.
מימוש
בגלל שאני לא יודע מה יהיה במערך אני מעדיף להשתמש ב Generics. בצורה כזאת אני יכול להחזיר בדיוק את האיבר מתוך המערך בלי לקלקל את הטיפוסים. בנוסף אני רוצה להפעיל את פונקציית המפתח רק פעם אחת על כל איבר כי אולי החישוב הוא מסובך או כבד. סך הכל המימוש יהיה:
function maxBy<T>(data: Array<T>, key: (t: T) => number): T {
if (data.length === 0) {
throw new Error("Array is empty");
}
return data
.map(i => ({ key: key(i), value: i }))
.reduce((max, value) => max.key > value.key ? max : value)
.value
}
חישבתי את פוקנציית המפתח על כל פריט במערך, שמרתי את התוצאות יחד עם הפריטים בתוך אוביקטים חדשים ובסוף החזרתי את האיבר שערך המפתח שלו היה הכי גבוה.
למה לשים לב
הדבר הראשון עליו אני מסתכל במימוש כזה הוא הטיפול במערך ריק. אין איבר מקסימלי במערך ריק ולכן צריך להחליט אם לזרוק שגיאה או להחזיר ערך ריק (null או משהו). אני מעדיף לזרוק שגיאה ושתהיה כמה שיותר ספציפית כדי שנבין מהר מה הטעות.
נקודה שניה כאן היא מספר הפעמים שצריך להפעיל את פונקציית המפתח. אנחנו רוצים לשים לב להפעיל אותה רק פעם אחת על כל איבר אחרת נקבל מימוש בזבזני. זה למשל קוד שעובד אבל מיותר:
function maxBy<T>(data: Array<T>, key: (t: T) => number): T {
return data
.reduce((max, value) => key(max) > key(value) ? max : value)
}
נקודה שלישית היא הטיפוסים. אנחנו רוצים שטייפסקריפט יוכל להבין מה הטיפוס של ערך ההחזר ולכן עדיף לשמור את המפתח והערך באוביקט ולא במערך. המימוש הזה למשל עובד אבל דורש הסבה ספציפית של הטיפוס:
function maxBy<T>(data: Array<T>, key: (t: T) => number): T {
if (data.length === 0) {
throw new Error("Array is empty");
}
return data
.map(i => [i, key(i)])
.reduce((max, value) => max[1] > value[1] ? max : value)
[0] as T
}