רק מרגיש ככה
"הקוד הזה פח, אי אפשר לתחזק אותו חייבים לזרוק הכל ולכתוב מחדש. אין בריה."
"שמע אני לא מבין על מה משלמים לנו בכלל. המודל העסקי של הסטארט-אפ הזה לא הגיוני והמוצר לא עובד. נס שהמשקיעים ממשיכים לשלם."
"תקשיבי ה Python הזה לא יתפוס. אין מצב שאנשים יעזבו את פרל בשביל שפה כל כך משעממת."
"אני לא מאמינה שהם לא התעניינו בפרויקט, איך הלכה חצי שנה. חבל שבכלל נכנסתי לזה, עדיף היה למצוא עבודה אמיתית."
"הפעם זה בטוח יצליח. בטוח."
לפעמים כשהברך כואבת זה בגלל שהולך לרדת גשם. לפעמים היא סתם כואבת. ולפעמים יש גשם בלי קשר לתחושה. מותר להרגיש כל דבר. חשוב להבדיל בין התחושה לדבר האמיתי.
"אני מרגיש שהקוד לא טוב. אלה הדברים שלדעתי לא עובדים בו. אלה הסיבות בגללן הקוד נכתב ככה אבל לדעתי סיבות אלה כבר לא רלוונטיות. אלה הדברים שאני חושב שכדאי לתקן ובסדר הזה. אגב אלה הדברים שכן אהבתי בקוד."
תבואו עם נתונים ותתרגלו להקשיב לאנשים שבאים עם נתונים. זה אולי פחות כיף אבל הרבה יותר פרודוקטיבי.
"הקוד הזה פח, אי אפשר לתחזק אותו חייבים לזרוק הכל ולכתוב מחדש. אין בריה."
"שמע אני לא מבין על מה משלמים לנו בכלל. המודל העסקי של הסטארט-אפ הזה לא הגיוני והמוצר לא עובד. נס שהמשקיעים ממשיכים לשלם."
"תקשיבי ה Python הזה לא יתפוס. אין מצב שאנשים יעזבו את פרל בשביל שפה כל כך משעממת."
"אני לא מאמינה שהם לא התעניינו בפרויקט, איך הלכה חצי שנה. חבל שבכלל נכנסתי לזה, עדיף היה למצוא עבודה אמיתית."
"הפעם זה בטוח יצליח. בטוח."
לפעמים כשהברך כואבת זה בגלל שהולך לרדת גשם. לפעמים היא סתם כואבת. ולפעמים יש גשם בלי קשר לתחושה. מותר להרגיש כל דבר. חשוב להבדיל בין התחושה לדבר האמיתי.
"אני מרגיש שהקוד לא טוב. אלה הדברים שלדעתי לא עובדים בו. אלה הסיבות בגללן הקוד נכתב ככה אבל לדעתי סיבות אלה כבר לא רלוונטיות. אלה הדברים שאני חושב שכדאי לתקן ובסדר הזה. אגב אלה הדברים שכן אהבתי בקוד."
תבואו עם נתונים ותתרגלו להקשיב לאנשים שבאים עם נתונים. זה אולי פחות כיף אבל הרבה יותר פרודוקטיבי.
🔥2
ואם אין before בספריית הבדיקה שלך?
בימים אלה אני בונה מחדש את קורס node.js שבאתר. הגירסה החדשה תכיל המון TypeScript ותכסה בנוסף ל node גם את Deno ו Bun והמטרה שלי היא שרוב הקורס יעבוד בכל שלושת סביבות הריצה.
בגדול המצב של TypeScript בצד שרת הוא מאוד טוב וגם דינו וגם באן מספיק בשלים בשביל לכתוב עליהם, אבל מדי פעם יש שטויות ובעיות תאימות. דוגמה קטנה היא שכשטוענים את המודול test של node מתוך deno אין תמיכה ב before.
ומה אם בכל זאת אנחנו רוצים להריץ קוד לפני בדיקה? נו, תמיד אפשר להיות יצירתיים. בדוגמה מהקורס רציתי להריץ קוד שמאתחל טבלה בבסיס נתונים בזיכרון לפני שאני מריץ קוד. בהשראה מ pytest כתבתי במקום before את הפונקציה הבאה:
עכשיו הבדיקה צריכה רק להפעיל את הפונקציה והיא מקבלת אוטומטית גם את קוד האיתחול וגם את קוד הניקוי. זה נראה ככה:
וכן המחשבה הראשונה שלי היתה שהחיים היו מושלמים אם דברים היו עובדים בכל הסביבות. אבל במחשבה שניה אני חושב שללמוד להסתדר גם כשדברים לא עובדים כמו בספר זו גם מיומנות חשובה ששווה להראות בקורס.
בימים אלה אני בונה מחדש את קורס node.js שבאתר. הגירסה החדשה תכיל המון TypeScript ותכסה בנוסף ל node גם את Deno ו Bun והמטרה שלי היא שרוב הקורס יעבוד בכל שלושת סביבות הריצה.
בגדול המצב של TypeScript בצד שרת הוא מאוד טוב וגם דינו וגם באן מספיק בשלים בשביל לכתוב עליהם, אבל מדי פעם יש שטויות ובעיות תאימות. דוגמה קטנה היא שכשטוענים את המודול test של node מתוך deno אין תמיכה ב before.
ומה אם בכל זאת אנחנו רוצים להריץ קוד לפני בדיקה? נו, תמיד אפשר להיות יצירתיים. בדוגמה מהקורס רציתי להריץ קוד שמאתחל טבלה בבסיס נתונים בזיכרון לפני שאני מריץ קוד. בהשראה מ pytest כתבתי במקום before את הפונקציה הבאה:
import { Database } from '@/db_types.ts'
import { Kysely } from 'kysely'
import { DenoSqliteDialect } from "@soapbox/kysely-deno-sqlite";
import { DB as Sqlite } from 'https://deno.land/x/sqlite/mod.ts';
export const useDB = async (test: (db: Kysely<Database>) => Promise<void>) => {
const _db = new Kysely<Database>({
dialect: new DenoSqliteDialect({
database: new Sqlite(':memory:'),
}),
});
await _db.schema
.createTable('contact_info')
.addColumn('id', 'integer', (col) => col.primaryKey())
.addColumn('name', 'text', (col) => col.notNull())
.addColumn('email', 'text', col => col.unique())
.execute()
try {
await test(_db);
} finally {
await _db.destroy();
}
}
עכשיו הבדיקה צריכה רק להפעיל את הפונקציה והיא מקבלת אוטומטית גם את קוד האיתחול וגם את קוד הניקוי. זה נראה ככה:
test('POST /contacts created a new contact', async () => {
await useDB(async db => {
await superdeno(app(db))
.post('/api/v1/contacts')
.set('Accept', 'application/json')
.send({name: "a", email: "a@gmail.com"})
.expect(200);
const res = await superdeno(app(db))
.get('/api/v1/contacts')
.set('Accept', 'application/json')
assert.deepEqual([
{ id: 1, name: "a", email: "a@gmail.com" }
], res.body);
})
});
וכן המחשבה הראשונה שלי היתה שהחיים היו מושלמים אם דברים היו עובדים בכל הסביבות. אבל במחשבה שניה אני חושב שללמוד להסתדר גם כשדברים לא עובדים כמו בספר זו גם מיומנות חשובה ששווה להראות בקורס.
בגדול זה עובד
ג'ואי צ'נג (אני מקווה שאני כותב את השם הזה נכון) עשתה עבודה מטורפת כדי לאפשר ל node.js לטעון עם require מודולים של ESM. היא כתבה על זה בבלוג שלה כאן:
https://joyeecheung.github.io/blog/2024/03/18/require-esm-in-node-js/
אין ספק שהאופן שבו אנחנו כותבים TypeScript ב node.js הוא עקום הרבה בגלל הסיפור הזה. בשביל שדברים יעבדו כמו שצריך ברוב הפרויקטים אנחנו כותבים קוד TypeScript שנראה כמו ESM, אבל אז מקמפלים אותו ל CJS בשביל ש node יריץ אותו, וזה בגדול עובד עד שמנסים לעשות דברים שיש רק ב ESM ואז הכל נשבר.
בקיצור ג'ואי צ'נג כתבה PR שמאפשר לקוד CJS לעשות require לקוד ESM, שזה כבר מאוד משפר את המצב להרבה מצבים. אבל זה עדיין עקום כי זה לא מטפל בבעיה האמיתית, שהיא הקומפילציה ל CJS רק בשביל שדברים יעבדו כמו שצריך עם מודולים ישנים ב npm.
(כי אם הכל היה ESM לא היינו צריכים לטעון ESM עם require).
אבל הנקודה הכי חשובה כאן היא מה לא עובד - למרות כל העבודה, למרות שהיא הצליחה לפתור הרבה בעיות לאנשים, היא עדיין השאיר נקודה פתוחה - ה await מחוץ לכל פונקציה לא יעבוד.
ופה יש התלבטות אמיתית שאנחנו מוצאים בהרבה מערכות וספריות וזה מעניין לראות איך הדברים האלה נוצרים. מי שכותבת את הקוד יודעת שיש לה מקרה שהיא לא רוצה לטפל בו, כי הוא מסובך ומחוץ לסקופ, אז היא מייצרת פיתרון שעובד רק ל 90% מהמקרים. שנתיים אחר כך מישהו מגיע למערכת הזאת ומנסה להריץ את אותם מקרים שלא נתמכים ולא מבין - איך זה לא עובד? הרי בכל התיעוד כתוב שאפשר לעשות XYZ. מה מיוחד באיך שאני עושה את זה? למה רק אצלי זה לא עובד?
במקרים כאלה התקשורת היא הכל. קוד שמטפל ב 90% מהמקרים צריך לזהות את ה 10% הנותרים ולהציג הודעת שגיאה מאוד מפורטת עדיף עם קישור להסבר מה מיוחד בדבר שניסית לעשות ולמה החלטנו לא לתמוך בזה. אין דבר יותר מתסכל מלשבת יומיים רק בשביל לגלות שהמקרה שלך נפל מחוץ לסקופ.
ג'ואי צ'נג (אני מקווה שאני כותב את השם הזה נכון) עשתה עבודה מטורפת כדי לאפשר ל node.js לטעון עם require מודולים של ESM. היא כתבה על זה בבלוג שלה כאן:
https://joyeecheung.github.io/blog/2024/03/18/require-esm-in-node-js/
אין ספק שהאופן שבו אנחנו כותבים TypeScript ב node.js הוא עקום הרבה בגלל הסיפור הזה. בשביל שדברים יעבדו כמו שצריך ברוב הפרויקטים אנחנו כותבים קוד TypeScript שנראה כמו ESM, אבל אז מקמפלים אותו ל CJS בשביל ש node יריץ אותו, וזה בגדול עובד עד שמנסים לעשות דברים שיש רק ב ESM ואז הכל נשבר.
בקיצור ג'ואי צ'נג כתבה PR שמאפשר לקוד CJS לעשות require לקוד ESM, שזה כבר מאוד משפר את המצב להרבה מצבים. אבל זה עדיין עקום כי זה לא מטפל בבעיה האמיתית, שהיא הקומפילציה ל CJS רק בשביל שדברים יעבדו כמו שצריך עם מודולים ישנים ב npm.
(כי אם הכל היה ESM לא היינו צריכים לטעון ESM עם require).
אבל הנקודה הכי חשובה כאן היא מה לא עובד - למרות כל העבודה, למרות שהיא הצליחה לפתור הרבה בעיות לאנשים, היא עדיין השאיר נקודה פתוחה - ה await מחוץ לכל פונקציה לא יעבוד.
ופה יש התלבטות אמיתית שאנחנו מוצאים בהרבה מערכות וספריות וזה מעניין לראות איך הדברים האלה נוצרים. מי שכותבת את הקוד יודעת שיש לה מקרה שהיא לא רוצה לטפל בו, כי הוא מסובך ומחוץ לסקופ, אז היא מייצרת פיתרון שעובד רק ל 90% מהמקרים. שנתיים אחר כך מישהו מגיע למערכת הזאת ומנסה להריץ את אותם מקרים שלא נתמכים ולא מבין - איך זה לא עובד? הרי בכל התיעוד כתוב שאפשר לעשות XYZ. מה מיוחד באיך שאני עושה את זה? למה רק אצלי זה לא עובד?
במקרים כאלה התקשורת היא הכל. קוד שמטפל ב 90% מהמקרים צריך לזהות את ה 10% הנותרים ולהציג הודעת שגיאה מאוד מפורטת עדיף עם קישור להסבר מה מיוחד בדבר שניסית לעשות ולמה החלטנו לא לתמוך בזה. אין דבר יותר מתסכל מלשבת יומיים רק בשביל לגלות שהמקרה שלך נפל מחוץ לסקופ.
Joyee Cheung's Blog
require(esm) in Node.js
Recently I landed experimental support for require()-ing synchronous ES modules in Node.js, a feature that has been long overdue. In the pull request, I commented with my understanding about why it di
שב רגע בצד טייפסקריפט, אני צריך לעבוד
נתבונן בשתי פונקציות בטייפסקריפט שמשתמשות במערכת הטיפוסים של קיסלי עבור גישה לבסיס נתונים SQL:
רואים את הדמיון? ברור שכן. שתיהן מוציאות שאילתה ראשונה כדי לקבל את המשתמש, ואז בונות שאילתה נוספת בשביל לעשות משהו עם המשתמש - פעם אחת למחוק מידע ופעם שניה לעדכן מידע.
ניסיון לאחד אותן ולבטל את הקוד המשותף עשוי להיראות כך:
הקוד הזה עובד ואפשר להשתמש בו בקלות למשל:
יש רק בעיה אחת, סימן אחד שחסר לי - מה מחזירה הפונקציה f ? מה לכתוב במקום סימני השאלה?
בעולם מתוקן טייפסקריפט היה מזהה שאני משתמש רק בחלקים משותפים מבין שני הממשקים שהפונקציות מחזירות ומאפשר לי לכתוב איחוד של הטיפוסים או אולי אפילו מבין את זה לבד. בעולם שלנו זה עוד אחד מהמצבים בהם הבאנו את טייפסקריפט לקצה.
עכשיו צריך לבחור, האם להתעקש על מערכת הטיפוסים של טייפסקריפט או להזיז אותה הצידה לרגע רק בשביל לתקן את הכפילות? הגישה שלי פה היא יותר פרגמטית. אם לא הצלחתי למצוא את הטיפוס שפותר לי את הבעיה אני שמח לכתוב any בתוך פונקציית עזר בשביל שאוכל להתקדם ולקצר את הקוד. תמיד אפשר יהיה להחליף את זה לבדיקת טיפוסים יותר ספציפית בעתיד כשטייפסקריפט יהיה מספיק חכם.
נתבונן בשתי פונקציות בטייפסקריפט שמשתמשות במערכת הטיפוסים של קיסלי עבור גישה לבסיס נתונים SQL:
async editNote(username: string, noteId: number, newText: string) {
const user = await db.selectFrom('users').selectAll().where('users.name', '=', username).executeTakeFirstOrThrow();
return db
.updateTable('notes')
.set('text', newText)
.where(noteBelongsToUser(user.id, noteId))
.returningAll()
.executeTakeFirstOrThrow()
},
async deleteNote(username: string, noteId: number) {
const user = await db.selectFrom('users').selectAll().where('users.name', '=', username).executeTakeFirstOrThrow();
return db
.deleteFrom('notes')
.where(noteBelongsToUser(user.id, noteId))
.returningAll()
.executeTakeFirstOrThrow()
},
רואים את הדמיון? ברור שכן. שתיהן מוציאות שאילתה ראשונה כדי לקבל את המשתמש, ואז בונות שאילתה נוספת בשביל לעשות משהו עם המשתמש - פעם אחת למחוק מידע ופעם שניה לעדכן מידע.
ניסיון לאחד אותן ולבטל את הקוד המשותף עשוי להיראות כך:
async function dry(db: Kysely<Database>,
username: string,
noteId: number,
f: (db: Kysely<Database>) => ???) {
const user = await db.selectFrom('users').selectAll().where('users.name', '=', username).executeTakeFirstOrThrow();
return f(db)
.where(noteBelongsToUser(user.id, noteId))
.returningAll()
.executeTakeFirstOrThrow()
}
הקוד הזה עובד ואפשר להשתמש בו בקלות למשל:
async easyDeleteNote(username: string, noteId: number) {
dry(db, username, noteId, (db) => db.deleteFrom('notes'))
}
יש רק בעיה אחת, סימן אחד שחסר לי - מה מחזירה הפונקציה f ? מה לכתוב במקום סימני השאלה?
בעולם מתוקן טייפסקריפט היה מזהה שאני משתמש רק בחלקים משותפים מבין שני הממשקים שהפונקציות מחזירות ומאפשר לי לכתוב איחוד של הטיפוסים או אולי אפילו מבין את זה לבד. בעולם שלנו זה עוד אחד מהמצבים בהם הבאנו את טייפסקריפט לקצה.
עכשיו צריך לבחור, האם להתעקש על מערכת הטיפוסים של טייפסקריפט או להזיז אותה הצידה לרגע רק בשביל לתקן את הכפילות? הגישה שלי פה היא יותר פרגמטית. אם לא הצלחתי למצוא את הטיפוס שפותר לי את הבעיה אני שמח לכתוב any בתוך פונקציית עזר בשביל שאוכל להתקדם ולקצר את הקוד. תמיד אפשר יהיה להחליף את זה לבדיקת טיפוסים יותר ספציפית בעתיד כשטייפסקריפט יהיה מספיק חכם.
פיתרון Advent Of Code יום 16 בסקאלה
עבר הרבה זמן מאז שפירסמתי את הפיתרון ליום 15 בסידרת Advent Of Code. אפשר לקרוא לזה משבר האמצע או עומס מסיבות אחרות. בכל מקרה היום נעשה עוד צעד בדרך לסיום כל 25 החידות עד סוף השנה.
האתגר - מבוך המראות
האתגר שלנו היום הוא לדמיין מבוך של מראות וקרן אור שעוברת בין המראות. זה קלט הדוגמה:
הקרן נכנסת מהפינה השמאלית עליונה למבוך ומתקדמת בכיוון ימין. ההתנהגות שלה תלויה בסוג המראה שהיא תפגוש:
1. אם היא פוגשת מראה אלכסונית היא תשנה את הכיוון לפי האלכסון (לדוגמה קרן שהולכת ימינה ופוגשת במראה
2. אם היא פוגשת מראה ישרה בצד בצד המחודד שלה לא קורה כלום.
3. אם היא פוגשת מראה ישרה בצד השטוח שלה, למשל קרן שהולכת ימינה ופוגשת במראה
האתגר שלנו הוא למצוא בכמה משבצות הקרן עברה.
פיתרון בסקאלה
הטריק החשוב כאן הוא להבין את תנאי העצירה. קרן מפסיקה לעניין אותנו כשהיא יוצאת מהמטריצה כמובן, אבל גם אם היא מגיעה שוב לנקודה שהיא כבר ביקרה בה ובאותו כיוון, כי במצב כזה היא נכנסת למעגל.
לכן הפונקציה המעניינת של הפיתרון היא:
זו פונקציה רקורסיבית שמקבלת רשימה של קרניים, מטריצה ורשימת מיקומים שביקרנו בהם ומחשבת את רשימת הקרניים הבאה. בשביל זה היא קוראת לפונקציית עזר בשם
אחרי שכל קרן התקדמה צעד אחד תהיה לנו רשימה של קרניים חדשות, אותן נשלח שוב ל
סך הכל הפיתרון בסקאלה לחלק הראשון הוא:
עבר הרבה זמן מאז שפירסמתי את הפיתרון ליום 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)
www.tocode.co.il
פיתרון Advent Of Code יום 15 בסקאלה - איזה כיף שהמציאו את ListMap
התרגיל של יום 15 ב Advent Of Code היה בגדול ממש קל, במיוחד בהשוואה לימים הקודמים עם המטריצות. בפוסט הפעם לא אכתוב את הסיפור המלא (בעיקר כי הוא עמוס בחישובים לא מעניינים) ונתמקד בחלק היחיד שכן היה מעניין ביום הזה - סידור העדשות בקופסאות.
❤1
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) {
// left
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))
אני חייב להודות שלמרות שהקוד ארוך זה היה אחד התרגילים הקלים של השנה, במיוחד אחרי שכבר היו לי את כל הפונקציות של פיענוח הקלט המטריציוני מתרגילים קודמים.
בואו נכתוב את maxBy ב TypeScript
הפונקציה
חתימה
אני רוצה לבנות פונקציה בשם
אני מדמיין שאני מפעיל אותה באופן הבא:
ומקבל את Fry שגילו 1031.
מימוש
בגלל שאני לא יודע מה יהיה במערך אני מעדיף להשתמש ב Generics. בצורה כזאת אני יכול להחזיר בדיוק את האיבר מתוך המערך בלי לקלקל את הטיפוסים. בנוסף אני רוצה להפעיל את פונקציית המפתח רק פעם אחת על כל איבר כי אולי החישוב הוא מסובך או כבד. סך הכל המימוש יהיה:
חישבתי את פוקנציית המפתח על כל פריט במערך, שמרתי את התוצאות יחד עם הפריטים בתוך אוביקטים חדשים ובסוף החזרתי את האיבר שערך המפתח שלו היה הכי גבוה.
למה לשים לב
הדבר הראשון עליו אני מסתכל במימוש כזה הוא הטיפול במערך ריק. אין איבר מקסימלי במערך ריק ולכן צריך להחליט אם לזרוק שגיאה או להחזיר ערך ריק (null או משהו). אני מעדיף לזרוק שגיאה ושתהיה כמה שיותר ספציפית כדי שנבין מהר מה הטעות.
נקודה שניה כאן היא מספר הפעמים שצריך להפעיל את פונקציית המפתח. אנחנו רוצים לשים לב להפעיל אותה רק פעם אחת על כל איבר אחרת נקבל מימוש בזבזני. זה למשל קוד שעובד אבל מיותר:
נקודה שלישית היא הטיפוסים. אנחנו רוצים שטייפסקריפט יוכל להבין מה הטיפוס של ערך ההחזר ולכן עדיף לשמור את המפתח והערך באוביקט ולא במערך. המימוש הזה למשל עובד אבל דורש הסבה ספציפית של הטיפוס:
הפונקציה
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
חמש בעיות מרכזיות שיש לי עם דינו היום
דינו הוא ההבטחה הגדולה הבאה אבל בינתיים ולמרות שהם כל הזמן נראים בכיוון הנכון יש עדיין כמה אתגרים משמעותיים למי שינסה לאמץ אותו ובמיוחד אם רוצים לשלב עבודה עם קוד ישן. אלה הבעיות המרכזיות שלי עם דינו היום -
1. מאגר חבילות - דינו תומכים ב JSR, ב NPM ובטעינה של כל קובץ חבילה מ denoland. אבל
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.js. חבל רק שההתעקשות שלהם על הדרך החדשה והנכונה לעשות דברים באה על חשבון נוחות של המשתמשים. המסע לאימוץ דינו הולך להיות ארוך וכנראה יחייב פרידה מספריות ישנות ומעבר לחדשות. זה אפשרי אבל זה לא יקרה מחר בבוקר ובינתיים עדיין קשה לראות את המוטיבציה של אנשים להחליף במיוחד כל עוד node.js ממשיך להיות מתוחזק.
דינו הוא ההבטחה הגדולה הבאה אבל בינתיים ולמרות שהם כל הזמן נראים בכיוון הנכון יש עדיין כמה אתגרים משמעותיים למי שינסה לאמץ אותו ובמיוחד אם רוצים לשלב עבודה עם קוד ישן. אלה הבעיות המרכזיות שלי עם דינו היום -
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 ממשיך להיות מתוחזק.
היום למדתי (שוב) - תמיד לסמן שגיאות
יש פה באתר מנגנון שמאפשר לכם לקבל כל פוסט חדש מהבלוג לאימייל. אבל אם נרשמתם ומכל מיני סיבות לא הצלחתי לשלוח לכם את המייל אני מבטל את הרישום כדי לא לשלוח סתם. עד אתמול זה היה הקוד שהיה אחראי על המנגנון:
בגדול המסלול התקין מופיע בפונקציה בצורה מאוד ברורה - אם קיבלנו הודעה שאי אפשר היה לשלוח את המייל אז נמחק את המנוי כדי שלא נצטרך לשלוח מיילים גם מחר. מסלול השגיאות זו כבר בעיה אחרת. הפונקציה נכתבה כדי להצליח תמיד, כי ההודעה מגיעה ב Webhook ולא אכפת לשרת המיילים ששלח את ההודעה אם מצאתי או לא מצאתי את המנוי עליו הוא מדווח.
אבל לי זה אכפת.
כי אם הם משנים את שם האירוע - במקרה שלנו זה השתנה מ bounced ל failed, אז החיפוש תמיד ייכשל אבל הכל יראה תקין, אפילו שהמערכת תתעלם מכל ההודעות על כשלונות. זה פשוט יראה כאילו כל שליחת מייל מצליחה.
הפיתרון הוא קל אבל האתגר לטווח הארוך הוא קשה: צריך לזכור תמיד שדברים יכולים להשתנות, וגם כשאנחנו מוכנים "להכיל" כשלונות עדיין לרשום אותם ולדווח עליהם. המערכת לא צריכה להתרסק ולא לגרום לתגובת שרשרת כשדברים רעים קורים, אבל כן כדאי לדווח על זה כדי שאפשר יהיה לתקן בזמן.
יש פה באתר מנגנון שמאפשר לכם לקבל כל פוסט חדש מהבלוג לאימייל. אבל אם נרשמתם ומכל מיני סיבות לא הצלחתי לשלוח לכם את המייל אני מבטל את הרישום כדי לא לשלוח סתם. עד אתמול זה היה הקוד שהיה אחראי על המנגנון:
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, אז החיפוש תמיד ייכשל אבל הכל יראה תקין, אפילו שהמערכת תתעלם מכל ההודעות על כשלונות. זה פשוט יראה כאילו כל שליחת מייל מצליחה.
הפיתרון הוא קל אבל האתגר לטווח הארוך הוא קשה: צריך לזכור תמיד שדברים יכולים להשתנות, וגם כשאנחנו מוכנים "להכיל" כשלונות עדיין לרשום אותם ולדווח עליהם. המערכת לא צריכה להתרסק ולא לגרום לתגובת שרשרת כשדברים רעים קורים, אבל כן כדאי לדווח על זה כדי שאפשר יהיה לתקן בזמן.
פונקציית Pipe ושרשור מתודות
בתיעוד של אמזון אנחנו מוצאים את הדוגמה הבאה לשימוש ב Polly ב Java:
התבנית הזאת נקראת Builder והיא מציעה טכניקה להתמודד עם בנאי שצריך לקבל הרבה פרמטרים. הרעיון הוא שבמקום להעביר את כל הפרמטרים בקריאה אחת בבנאי אנחנו נפעיל עוד ועוד פונקציות על האוביקט כשכל פונקציה מגדירה עוד פרמטר לבניית הדבר שאנחנו רוצים לבנות. הייתרון בתבנית ה Builder הוא שאפשר לבנות את הדבר בשלבים ואפילו לשלב באמצע תנאים או לולאות.
תבנית דומה לה נקראת Fluent Interface והיא מציעה שימוש בשרשור מתודות כדי לתאר פונקציונאליות או רצף פעולות. לדוגמה הקוד הבא מספריית jQuery:
התבנית מתארת ממשק בצורה נוחה של קריאות בשרשרת לפונקציות השונות של האוביקט. כמו ב Builder, גם ב Fluent Interface כל פונקציה מחזירה את האוביקט שעליו אנחנו עובדים וכך אפשר לחבר עוד ועוד פעולות.
אבל הבעיה בתבנית הזאת ובכל שרשור של פונקציות היא שקשה לראות איך לחבר את זה לתנאים ולולאות שאנחנו מכירים. בדוגמה של ה jQuery אם הייתי רוצה להפעיל פעולה 10 פעמים ברצף הייתי צריך לכתוב אותה ממש 10 פעמים, או לשמור את מצב הביניים של השרשרת למשתנה כדי שאוכל להמשיך את השרשרת על המשתנה בתוך הלולאה.
טכניקה פשוטה להתמודד עם לולאות בתוך שרשראות של פונקציות היא הפונקציה
בואו ניקח דוגמה מ Ruby שם tap כבר מובנית בשפה ונראה איך להשתמש בה כדי להוסיף לולאות לשרשראות של פונקציות. אני מתחיל עם מחלקה בשם Polly שעובדת בתבנית הבנאי עם הקוד הבא:
ועכשיו אני רוצה להפעיל את
אבל אם רוצים לוותר על המשתנה אפשר להשתמש ב tap ואז נקבל:
בתיעוד של אמזון אנחנו מוצאים את הדוגמה הבאה לשימוש ב 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) }