ToCode
Open in Telegram
1 419
Subscribers
No data24 hours
No data7 days
-530 days
Posts Archive
1 420
נשים לב ש 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 420
פיתוח קוד שמתאים גם ל 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 420
לא נראה כמו 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 420
טיפ 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 420
פונקציית 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 420
היום למדתי (שוב) - תמיד לסמן שגיאות
יש פה באתר מנגנון שמאפשר לכם לקבל כל פוסט חדש מהבלוג לאימייל. אבל אם נרשמתם ומכל מיני סיבות לא הצלחתי לשלוח לכם את המייל אני מבטל את הרישום כדי לא לשלוח סתם. עד אתמול זה היה הקוד שהיה אחראי על המנגנון:
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 420
חמש בעיות מרכזיות שיש לי עם דינו היום
דינו הוא ההבטחה הגדולה הבאה אבל בינתיים ולמרות שהם כל הזמן נראים בכיוון הנכון יש עדיין כמה אתגרים משמעותיים למי שינסה לאמץ אותו ובמיוחד אם רוצים לשלב עבודה עם קוד ישן. אלה הבעיות המרכזיות שלי עם דינו היום -
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 420
בואו נכתוב את 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
}1 420
List(Beam(row, col, Direction.Right))
} else if (row == numRows) {
// bottom
List(Beam(row, col, Direction.Up))
} else if (col == numColumns) {
// right
List(Beam(row, col, Direction.Left))
} else {
List()
}
}.toList
@main
def day16part2(): Unit =
val matrix = parseInput(Source.fromResource("day16.txt"))
val bestBeam = border(matrix).maxBy { b => countVisited(b, matrix) }
println(countVisited(bestBeam, matrix))
אני חייב להודות שלמרות שהקוד ארוך זה היה אחד התרגילים הקלים של השנה, במיוחד אחרי שכבר היו לי את כל הפונקציות של פיענוח הקלט המטריציוני מתרגילים קודמים.1 420
def right(): Beam = Beam(this.row, this.column + 1, Direction.Right)
}
val demoInput: String =
""".|...\....
||.-.\.....
|.....|-...
|........|.
|..........
|.........\
|..../.\\..
|.-.-/..|..
|.|....-|.\
|..//.|....
|""".stripMargin
def step(beam: Beam, ch: Char): List[Beam] =
ch match
case '.' => List(beam.continue())
case '|' if vertical(beam.direction) => List(beam.continue())
case '|' if horizontal(beam.direction) => List(beam.up(), beam.down())
case '-' if horizontal(beam.direction) => List(beam.continue())
case '-' if vertical(beam.direction) => List(beam.left(), beam.right())
case '/' if beam.direction == Direction.Right => List(beam.up())
case '/' if beam.direction == Direction.Left => List(beam.down())
case '/' if beam.direction == Direction.Up => List(beam.right())
case '/' if beam.direction == Direction.Down => List(beam.left())
case '\\' if beam.direction == Direction.Right => List(beam.down())
case '\\' if beam.direction == Direction.Left => List(beam.up())
case '\\' if beam.direction == Direction.Up => List(beam.left())
case '\\' if beam.direction == Direction.Down => List(beam.right())
@tailrec
def travel(beams: List[Beam],
matrix: Map[(Int, Int), Char],
visited: Set[Beam] = Set()): Set[Beam] =
val activeBeams = beams.filter { b =>
matrix.contains(b.row, b.column) && !visited.contains(b)
}
if (activeBeams.nonEmpty) {
val nextRound = activeBeams.flatMap { b => step(b, matrix(b.row, b.column)) }
travel(nextRound, matrix, visited ++ activeBeams.toSet)
} else {
visited
}
def parseInput(input: Source): Map[(Int, Int), Char] =
input
.getLines()
.zipWithIndex
.collect {
case (line: String, index: Int) =>
line.toList.zipWithIndex.map((ch, column) => (index, column, ch))
}
.flatten
.flatMap { case (row, column, ch) => Map((row, column) -> ch) }
.toMap
def printMatrix(matrix: Map[(Int, Int), Char]): Unit =
val maxRow = matrix.keys.maxBy(_._1)._1
val maxColumn = matrix.keys.maxBy(_._2)._2
0.to(maxRow).foreach { row =>
0.to(maxColumn).foreach { col =>
print(matrix((row, col)))
}
println()
}
def countVisited(start: Beam, matrix: Map[(Int, Int), Char]) =
val visited = travel(List(start), matrix)
visited.map(b => (b.row, b.column)).size
@main
def day16part1(): Unit =
val matrix = parseInput(Source.fromResource("day16.txt"))
println(countVisited(Beam(0, 0, Direction.Right), matrix))
}
בחלק השני ביקשו למצוא נקודת כניסה חלופית לקרן ממנה אפשר יהיה לבקר ביותר משבצות. אני לא מצאתי טריק חכם ופשוט חישבתי בכמה משבצות מבקרים מכל נקודת כניסה אפשרית והחזרתי את זו שהמספר עבורה היה הגדול ביותר. זה הקוד:
def border(matrix: Map[(Int, Int), Char]): List[Beam] =
val numRows = matrix.keys.maxBy(_._1)._1
val numColumns = matrix.keys.maxBy(_._2)._2
matrix.keys.filter { (row, col) =>
row == 0 || row == numRows || col == 0 || col == numColumns
}.flatMap { (row, col) =>
if (row == 0 && col == 0) {
// top left
List(Beam(0, 0, Direction.Down), Beam(0, 0, Direction.Right))
} else if (row == 0 && col == numColumns) {
// top right
List(Beam(row, col, Direction.Down), Beam(row, col, Direction.Left))
} else if (row == numRows && col == 0) {
// bottom left
List(Beam(row, col, Direction.Up), Beam(row, col, Direction.Right))
} else if (row == numRows && col == numColumns) {
// bottom right
List(Beam(row, col, Direction.Up), Beam(row, col, Direction.Left))
} else if (row == 0) {
// top row
List(Beam(row, col, Direction.Down))
} else if (col == 0) {
// left1 420
פיתרון Advent Of Code יום 16 בסקאלה
עבר הרבה זמן מאז שפירסמתי את הפיתרון ליום 15 בסידרת Advent Of Code. אפשר לקרוא לזה משבר האמצע או עומס מסיבות אחרות. בכל מקרה היום נעשה עוד צעד בדרך לסיום כל 25 החידות עד סוף השנה.
האתגר - מבוך המראות
האתגר שלנו היום הוא לדמיין מבוך של מראות וקרן אור שעוברת בין המראות. זה קלט הדוגמה:
.|...\....
|.-.\.....
.....|-...
........|.
..........
.........\
..../.\\..
.-.-/..|..
.|....-|.\
..//.|....
הקרן נכנסת מהפינה השמאלית עליונה למבוך ומתקדמת בכיוון ימין. ההתנהגות שלה תלויה בסוג המראה שהיא תפגוש:
1. אם היא פוגשת מראה אלכסונית היא תשנה את הכיוון לפי האלכסון (לדוגמה קרן שהולכת ימינה ופוגשת במראה / תשנה את הכיוון למעלה).
2. אם היא פוגשת מראה ישרה בצד בצד המחודד שלה לא קורה כלום.
3. אם היא פוגשת מראה ישרה בצד השטוח שלה, למשל קרן שהולכת ימינה ופוגשת במראה |, אז הקרן תתפצל ל-2 והקרניים ילכו לשני הצדדים למעלה ולמטה.
האתגר שלנו הוא למצוא בכמה משבצות הקרן עברה.
פיתרון בסקאלה
הטריק החשוב כאן הוא להבין את תנאי העצירה. קרן מפסיקה לעניין אותנו כשהיא יוצאת מהמטריצה כמובן, אבל גם אם היא מגיעה שוב לנקודה שהיא כבר ביקרה בה ובאותו כיוון, כי במצב כזה היא נכנסת למעגל.
לכן הפונקציה המעניינת של הפיתרון היא:
@tailrec
def travel(beams: List[Beam],
matrix: Map[(Int, Int), Char],
visited: Set[Beam] = Set()): Set[Beam] =
val activeBeams = beams.filter { b =>
matrix.contains(b.row, b.column) && !visited.contains(b)
}
if (activeBeams.nonEmpty) {
val nextRound = activeBeams.flatMap { b => step(b, matrix(b.row, b.column)) }
travel(nextRound, matrix, visited ++ activeBeams.toSet)
} else {
visited
}
זו פונקציה רקורסיבית שמקבלת רשימה של קרניים, מטריצה ורשימת מיקומים שביקרנו בהם ומחשבת את רשימת הקרניים הבאה. בשביל זה היא קוראת לפונקציית עזר בשם step שזה המימוש שלה:
def step(beam: Beam, ch: Char): List[Beam] =
ch match
case '.' => List(beam.continue())
case '|' if vertical(beam.direction) => List(beam.continue())
case '|' if horizontal(beam.direction) => List(beam.up(), beam.down())
case '-' if horizontal(beam.direction) => List(beam.continue())
case '-' if vertical(beam.direction) => List(beam.left(), beam.right())
case '/' if beam.direction == Direction.Right => List(beam.up())
case '/' if beam.direction == Direction.Left => List(beam.down())
case '/' if beam.direction == Direction.Up => List(beam.right())
case '/' if beam.direction == Direction.Down => List(beam.left())
case '\\' if beam.direction == Direction.Right => List(beam.down())
case '\\' if beam.direction == Direction.Left => List(beam.up())
case '\\' if beam.direction == Direction.Up => List(beam.left())
case '\\' if beam.direction == Direction.Down => List(beam.right())
אחרי שכל קרן התקדמה צעד אחד תהיה לנו רשימה של קרניים חדשות, אותן נשלח שוב ל travel וכך הלאה עד שלא יהיו יותר קרניים ברשימה.
סך הכל הפיתרון בסקאלה לחלק הראשון הוא:
import aoc2023day16.Direction
import aoc2023day16.Direction.{Down, Up}
import scala.annotation.tailrec
import scala.io.Source
object aoc2023day16 {
enum Direction {
case Up, Down, Left, Right
}
def vertical(dir: Direction): Boolean = dir == Direction.Up || dir == Direction.Down
def horizontal(dir: Direction): Boolean = dir == Direction.Left || dir == Direction.Right
case class Beam(row: Int, column: Int, direction: Direction) {
def continue(): Beam = {
this.direction match
case Direction.Right => right()
case Direction.Left => left()
case Direction.Up => up()
case Direction.Down => down()
}
def up(): Beam = Beam(this.row - 1, this.column, Direction.Up)
def down(): Beam = Beam(this.row + 1, this.column, Direction.Down)
def left(): Beam = Beam(this.row, this.column - 1, Direction.Left)
Available now! Telegram Research 2025 — the year's key insights 
