לא ככה משתמשים בזה
אין לי ספק שלסטיב ג'ובס היו כוונות טובות כשהוא האשים את המשתמשים שמחזיקים לא נכון את הטלפון שלו. הוא ראה את כל השעות שהם בילו במעבדות בבדיקות, את כל הנסיינים שניסו כל פונקציה בטלפון, את הבדיקות האוטומטיות, את הלוגים, את מנגנוני ההגנה והרגולציות.
והנה המוצר יוצא לשוק והמשתמשים עושים איתו דברים שאף אחד לא חשב עליהם בזמן הפיתוח.
מאיפה יש להם את החוצפה?
וזה לא רק האייפון...
איך הם לוחצים על כפתור פעמיים לפני שסיימתי לטפל בלחיצה הראשונה?
למה הם שומרים את הסטייט כל כך גבוה בעץ הקומפוננטות? הרי אמרתי לכם שזה יביא ליותר מדי רנדרים.
למה הם מנסים לתשאל את בסיס הנתונים בלי אינדקסים? ברור שזה יעבוד לאט.
מה פתאום הם בונים שאילתת SQL מקלט שמגיע בטופס? הרי אמרתי להם שצריך לנקות את הקלט לפני שמשתמשים בו.
"לא ככה משתמשים בזה" לא עובד. אנשים ימשיכו להשתמש לא נכון במוצרים שלנו, וימשיכו לבוא בטענות. כדאי לזכור:
1. בתור משתמשים חשוב לדעת איך היוצר המקורי תכנן שנשתמש במוצר. לא חייבים להשתמש בפריימוורק לפי החוקים אבל חשוב להכיר ולהבין אותם כדי להבין איך בדיוק זה יישבר כשנחזיק את זה לא נכון.
2. בתור יוצרים עלינו להיות מוכנים לביקורת על המוצרים שלנו ויש לנו את הבחירה - לתקן או להתעקש. ריאקט אומנם מתעקשים על מנגנון ניהול הסטייט שלהם אבל מנגנוני ORM פתרו כמעט לגמרי את הבעיה שאנשים בונים שאילתות SQL בצורה ידנית ולא מאובטחת. גם כשמחליטים "להתעקש" אפשר לוודא שהמשתמשים שלנו מבינים מה הם עושים לא בסדר ואיך מתקנים. בריאקט בגלל זה דואגים להדפיס אזהרה כל פעם שאנחנו מרנדרים קומפוננטות בלולאה בלי key. בסיס הנתונים JanusGraph מדפיס לי אזהרה ללוג כל פעם שאני שולח שאילתה ואין לו אינדקס מתאים בשבילה.
בסוף ברור שזה סיפור של תיאום ציפיות - אף אחד לא מצפה לזרוק את האייפון מקומה רביעית ושימשיך לעבוד, אבל אנחנו כן מצפים שנוכל להחזיק אותו איך שנרצה ועדיין להוציא שיחות. ככל שנתקשר טוב יותר עם המשתמשים שלנו ונבין את הציפיות שלהם נוכל לבנות מוצרים שיתאמו ואף יתעלו מעל אותן ציפיות.
אין לי ספק שלסטיב ג'ובס היו כוונות טובות כשהוא האשים את המשתמשים שמחזיקים לא נכון את הטלפון שלו. הוא ראה את כל השעות שהם בילו במעבדות בבדיקות, את כל הנסיינים שניסו כל פונקציה בטלפון, את הבדיקות האוטומטיות, את הלוגים, את מנגנוני ההגנה והרגולציות.
והנה המוצר יוצא לשוק והמשתמשים עושים איתו דברים שאף אחד לא חשב עליהם בזמן הפיתוח.
מאיפה יש להם את החוצפה?
וזה לא רק האייפון...
איך הם לוחצים על כפתור פעמיים לפני שסיימתי לטפל בלחיצה הראשונה?
למה הם שומרים את הסטייט כל כך גבוה בעץ הקומפוננטות? הרי אמרתי לכם שזה יביא ליותר מדי רנדרים.
למה הם מנסים לתשאל את בסיס הנתונים בלי אינדקסים? ברור שזה יעבוד לאט.
מה פתאום הם בונים שאילתת SQL מקלט שמגיע בטופס? הרי אמרתי להם שצריך לנקות את הקלט לפני שמשתמשים בו.
"לא ככה משתמשים בזה" לא עובד. אנשים ימשיכו להשתמש לא נכון במוצרים שלנו, וימשיכו לבוא בטענות. כדאי לזכור:
1. בתור משתמשים חשוב לדעת איך היוצר המקורי תכנן שנשתמש במוצר. לא חייבים להשתמש בפריימוורק לפי החוקים אבל חשוב להכיר ולהבין אותם כדי להבין איך בדיוק זה יישבר כשנחזיק את זה לא נכון.
2. בתור יוצרים עלינו להיות מוכנים לביקורת על המוצרים שלנו ויש לנו את הבחירה - לתקן או להתעקש. ריאקט אומנם מתעקשים על מנגנון ניהול הסטייט שלהם אבל מנגנוני ORM פתרו כמעט לגמרי את הבעיה שאנשים בונים שאילתות SQL בצורה ידנית ולא מאובטחת. גם כשמחליטים "להתעקש" אפשר לוודא שהמשתמשים שלנו מבינים מה הם עושים לא בסדר ואיך מתקנים. בריאקט בגלל זה דואגים להדפיס אזהרה כל פעם שאנחנו מרנדרים קומפוננטות בלולאה בלי key. בסיס הנתונים JanusGraph מדפיס לי אזהרה ללוג כל פעם שאני שולח שאילתה ואין לו אינדקס מתאים בשבילה.
בסוף ברור שזה סיפור של תיאום ציפיות - אף אחד לא מצפה לזרוק את האייפון מקומה רביעית ושימשיך לעבוד, אבל אנחנו כן מצפים שנוכל להחזיק אותו איך שנרצה ועדיין להוציא שיחות. ככל שנתקשר טוב יותר עם המשתמשים שלנו ונבין את הציפיות שלהם נוכל לבנות מוצרים שיתאמו ואף יתעלו מעל אותן ציפיות.
כמה מתכנתים צריך בשביל להתחבר ל S3?
מתכנת חכם אחד בנה תשתית משוגעת לחיבור ל S3 בתור POC. הוא רצה להראות ששמירת הקבצים על S3 תוכל לשפר את הביצועים במערכת ולאפשר גדילה אופקית. הפרויקט אומנם נקבר בגלל חילופי הנהלה אבל הבסיס של הקוד כבר הוכנס למערכת.
מתכנת חכם שני קיבל בקשה מלקוח לחבר לחשבון שלו את הקבצים שכבר שמורים ללקוח על S3. אצלו בצוות מתכנתים קיבלו קרדיט לפי כמה באגים הם פותרים ולכן היה חשוב לגרום לדברים לעבוד מהר ולהמשיך לסיפור הבא. הוא ידע שיש איזה תשתית בסיסית איפשהו אבל למי היה זמן להבין איך זה עובד אז הוא בנה מנגנון חדש וממוקד שעבד ממש בסדר.
מתכנת חכם שלישי גילה שמישהו כבר בנה ספרייה שמתחברת עם S3 וגם בונה אבסטרקציה כללית מעל איחסון קבצים בענן (כך שאפשר יהיה לעבור מהר לעננים אחרים). הוא החליט לממש באמצעותה חיבור ל S3 עבור פיצ'ר חדש אבל לא רצה להסתכן בלשבור קוד ישן ולא בדוק, ולכן לא ביצע מיגרציה מלאה.
מנהל חכם אחד שמע שהרבה לקוחות רוצים לאחסן קבצים על S3 וידע שעכשיו Micro Services הם ה-דבר, אז הוא מצא צוות באוקראינה שיבנו מיקרו סרביס עם חיבור ל S3. נכון שהסרביס כתוב ב Erlang אבל הבטיחו לו שהביצועים יהיו מצוינים ושהאוקראינים יוכלו לפתור כל בעיה בלי לגעת בשאר המערכת.
מתכנת חכם רביעי היה צריך לעדכן את מנגנון אחסון הקבצים של הלקוחות. האוקראינים לא זמינים כבר שנתיים וכאן אף אחד לא יודע Erlang. את הספריה שהשלישי הכניס הוא לא הכיר וממילא היא כבר לא נתמכת ושני המנגנונים האחרים לא היו מספיק גנריים או קלים לשימוש. מזל שבדיוק יצאה ספריית חיבור חדשה ל S3 עם ממשק חדשני ואבטחת מידע משופרת.
כך נראה חוב טכני. כך נראית מערכת שכבר קשה להתקדם עם פיצ'רים כי כל שינוי שובר משהו אחר. זה הפיך אבל דורש עבודה והיום הוא בדיוק יום מצוין להתחיל לאחד מנגנונים.
מתכנת חכם אחד בנה תשתית משוגעת לחיבור ל S3 בתור POC. הוא רצה להראות ששמירת הקבצים על S3 תוכל לשפר את הביצועים במערכת ולאפשר גדילה אופקית. הפרויקט אומנם נקבר בגלל חילופי הנהלה אבל הבסיס של הקוד כבר הוכנס למערכת.
מתכנת חכם שני קיבל בקשה מלקוח לחבר לחשבון שלו את הקבצים שכבר שמורים ללקוח על S3. אצלו בצוות מתכנתים קיבלו קרדיט לפי כמה באגים הם פותרים ולכן היה חשוב לגרום לדברים לעבוד מהר ולהמשיך לסיפור הבא. הוא ידע שיש איזה תשתית בסיסית איפשהו אבל למי היה זמן להבין איך זה עובד אז הוא בנה מנגנון חדש וממוקד שעבד ממש בסדר.
מתכנת חכם שלישי גילה שמישהו כבר בנה ספרייה שמתחברת עם S3 וגם בונה אבסטרקציה כללית מעל איחסון קבצים בענן (כך שאפשר יהיה לעבור מהר לעננים אחרים). הוא החליט לממש באמצעותה חיבור ל S3 עבור פיצ'ר חדש אבל לא רצה להסתכן בלשבור קוד ישן ולא בדוק, ולכן לא ביצע מיגרציה מלאה.
מנהל חכם אחד שמע שהרבה לקוחות רוצים לאחסן קבצים על S3 וידע שעכשיו Micro Services הם ה-דבר, אז הוא מצא צוות באוקראינה שיבנו מיקרו סרביס עם חיבור ל S3. נכון שהסרביס כתוב ב Erlang אבל הבטיחו לו שהביצועים יהיו מצוינים ושהאוקראינים יוכלו לפתור כל בעיה בלי לגעת בשאר המערכת.
מתכנת חכם רביעי היה צריך לעדכן את מנגנון אחסון הקבצים של הלקוחות. האוקראינים לא זמינים כבר שנתיים וכאן אף אחד לא יודע Erlang. את הספריה שהשלישי הכניס הוא לא הכיר וממילא היא כבר לא נתמכת ושני המנגנונים האחרים לא היו מספיק גנריים או קלים לשימוש. מזל שבדיוק יצאה ספריית חיבור חדשה ל S3 עם ממשק חדשני ואבטחת מידע משופרת.
כך נראה חוב טכני. כך נראית מערכת שכבר קשה להתקדם עם פיצ'רים כי כל שינוי שובר משהו אחר. זה הפיך אבל דורש עבודה והיום הוא בדיוק יום מצוין להתחיל לאחד מנגנונים.
xkcd
Standards
👍3🔥3
סטייט ובחירת קומפוננטות ריאקט
ריאקט התחילה בתור ספריית UI ולכן זה לא היה מוזר לחשוב על קומפוננטה בתור "החלק בעמוד שמשתנה יחד". הסטייט של הקומפוננטה הוא המידע ששינוי שלו מוביל לשינוי ב UI ואם יש מידע שצריך להשפיע על כמה קומפוננטות נשמור אותו באיזשהו מקום חיצוני (פלאקס, זוכרים?) ונחבר את הקומפוננטות לשם. הדבר החשוב בחלוקה לקומפוננטות היה להיות מסוגלים לעשות Reasoning על החלקים בעמוד. להיות מסוגלים להבין למה הופיע עכשיו הסימן שיש הודעה חדשה.
אבל כל זה היה הגיוני ב 2015. אולי ב 2018.
היום ובמיוחד מאז Hooks אנחנו חושבים על קומפוננטה בתור הדבר בעמוד שחוזר על עצמו בכל מיני מקומות והקשרים. בתור ה Building Block של ארכיטקטורה מבוססת קומפוננטות. משהו שיש לו לוגיקה, משהו שמזיז דברים ביישום. ריאקט, למרות מה שהם כותבים בכותרת, היא כבר לא ספריה רק ל UI או לפחות אנשים לא משתמשים בה רק באופן הזה.
וכך נולד הקונפליקט- האם ליצור קומפוננטות לפי סמיכות מבחינת סטייט או לפי ממשק חיצוני ואפשרות לשימוש חוזר? לפעמים שני השיקולים האלה הולכים יחד, לפעמים הם מתנגשים, ובכל מקרה צורת החשיבה שמובילה לכל אחד שונה.
להשתמש בסטייט בתור קו מנחה לבחירת קומפוננטות זה קשה כי האינטואיציה שלנו כמתכנתים מחפשת "לסדר" את הקוד לאלמנטים לוגיים. במקרה של ריאקט זה שווה את המאמץ לחשוב כמו מפתחי UI.
ריאקט התחילה בתור ספריית UI ולכן זה לא היה מוזר לחשוב על קומפוננטה בתור "החלק בעמוד שמשתנה יחד". הסטייט של הקומפוננטה הוא המידע ששינוי שלו מוביל לשינוי ב UI ואם יש מידע שצריך להשפיע על כמה קומפוננטות נשמור אותו באיזשהו מקום חיצוני (פלאקס, זוכרים?) ונחבר את הקומפוננטות לשם. הדבר החשוב בחלוקה לקומפוננטות היה להיות מסוגלים לעשות Reasoning על החלקים בעמוד. להיות מסוגלים להבין למה הופיע עכשיו הסימן שיש הודעה חדשה.
אבל כל זה היה הגיוני ב 2015. אולי ב 2018.
היום ובמיוחד מאז Hooks אנחנו חושבים על קומפוננטה בתור הדבר בעמוד שחוזר על עצמו בכל מיני מקומות והקשרים. בתור ה Building Block של ארכיטקטורה מבוססת קומפוננטות. משהו שיש לו לוגיקה, משהו שמזיז דברים ביישום. ריאקט, למרות מה שהם כותבים בכותרת, היא כבר לא ספריה רק ל UI או לפחות אנשים לא משתמשים בה רק באופן הזה.
וכך נולד הקונפליקט- האם ליצור קומפוננטות לפי סמיכות מבחינת סטייט או לפי ממשק חיצוני ואפשרות לשימוש חוזר? לפעמים שני השיקולים האלה הולכים יחד, לפעמים הם מתנגשים, ובכל מקרה צורת החשיבה שמובילה לכל אחד שונה.
להשתמש בסטייט בתור קו מנחה לבחירת קומפוננטות זה קשה כי האינטואיציה שלנו כמתכנתים מחפשת "לסדר" את הקוד לאלמנטים לוגיים. במקרה של ריאקט זה שווה את המאמץ לחשוב כמו מפתחי UI.
פיתרון Advent Of Code 2023 יום 14 בסקאלה (חלק ראשון)
עוד שבוע מתחיל וסופי שבוע הם זמן מצוין לפתור עוד תרגיל של Advent Of Code. הפעם אנחנו ביום 14 ועדיין עם מטריצות.
התרגיל
נתון לוח עם סלעים, חלקם עגולים שמתגלגלים וחלקם מרובעים שלא זזים. ככה זה נראה:
עכשיו אנחנו מטים את הלוח קצת למעלה, אז כל הסלעים העגולים מתגלגלים למעלה ואנחנו מקבלים את הצורה הזאת:
המרובעים לא זזו כמו שדיברנו והעגולים התגלגלו עד שנעצרו בגלל סלע אחר.
עכשיו נסמן כל שורה במספר ונרצה לדעת כמה כדורים יש בכל שורה. בשביל להגיע למספר אחד כופלים את המספר של השורה במספר הכדורים שבה וסוכמים את כל התוצאות. עם המספרים זה נראה ככה:
יש 5 כדורים בשורה של 10 אז זה נותן 50, עוד שני כדורים בשורה של 9 שמוסיפים 18, וככה ממשיכים ומחברים עד שמקבלים את הסכום 136.
המשימה היא לכתוב תוכנית שמקבלת את המטריצה ומחשבת את הסכום אחרי כל הטלטלה.
פיתרון בסקאלה
הדבר הראשון שקופץ לעין כאן הוא שההזזה קורית לכל טור בנפרד, ושבתוך הטור יחסית קל להבין איך הוא יראה אחרי ההטיה למעלה - פשוט חותכים את הטור בכל סולמית, מזיזים את כל העיגולים להתחלה והנקודות לסוף ואז מחברים חזרה. בסקאלה זה יהיה פשוט:
ואז הקוד לטיפול בהטיה למעלה ייקח עמודה עמודה, יזיז את הסלעים בה למעלה ובסוף יחשב את הסכום לפי החישוב בתרגיל. זה הסיפור בסקאלה:
מחשבות על חלק 2
עוד שבוע מתחיל וסופי שבוע הם זמן מצוין לפתור עוד תרגיל של Advent Of Code. הפעם אנחנו ביום 14 ועדיין עם מטריצות.
התרגיל
נתון לוח עם סלעים, חלקם עגולים שמתגלגלים וחלקם מרובעים שלא זזים. ככה זה נראה:
O....#....
O.OO#....#
.....##...
OO.#O....O
.O.....O#.
O.#..O.#.#
..O..#O..O
.......O..
#....###..
#OO..#....
עכשיו אנחנו מטים את הלוח קצת למעלה, אז כל הסלעים העגולים מתגלגלים למעלה ואנחנו מקבלים את הצורה הזאת:
OOOO.#.O..
OO..#....#
OO..O##..O
O..#.OO...
........#.
..#....#.#
..O..#.O.O
..O.......
#....###..
#....#....
המרובעים לא זזו כמו שדיברנו והעגולים התגלגלו עד שנעצרו בגלל סלע אחר.
עכשיו נסמן כל שורה במספר ונרצה לדעת כמה כדורים יש בכל שורה. בשביל להגיע למספר אחד כופלים את המספר של השורה במספר הכדורים שבה וסוכמים את כל התוצאות. עם המספרים זה נראה ככה:
OOOO.#.O.. 10
OO..#....# 9
OO..O##..O 8
O..#.OO... 7
........#. 6
..#....#.# 5
..O..#.O.O 4
..O....... 3
* ....###.. 2 *
* ....#.... 1 *
יש 5 כדורים בשורה של 10 אז זה נותן 50, עוד שני כדורים בשורה של 9 שמוסיפים 18, וככה ממשיכים ומחברים עד שמקבלים את הסכום 136.
המשימה היא לכתוב תוכנית שמקבלת את המטריצה ומחשבת את הסכום אחרי כל הטלטלה.
פיתרון בסקאלה
הדבר הראשון שקופץ לעין כאן הוא שההזזה קורית לכל טור בנפרד, ושבתוך הטור יחסית קל להבין איך הוא יראה אחרי ההטיה למעלה - פשוט חותכים את הטור בכל סולמית, מזיזים את כל העיגולים להתחלה והנקודות לסוף ואז מחברים חזרה. בסקאלה זה יהיה פשוט:
"OO.O.O..##".split('#').map(_.sorted.reverse).mkString("#")
ואז הקוד לטיפול בהטיה למעלה ייקח עמודה עמודה, יזיז את הסלעים בה למעלה ובסוף יחשב את הסכום לפי החישוב בתרגיל. זה הסיפור בסקאלה:
import scala.annotation.tailrec
import scala.io.Source
import scala.util.chaining.*
object aoc2023day14 {
val demoInput: String =
"""O....#....
|O.OO#....#
|.....##...
|OO.#O....O
|.O.....O#.
|O.#..O.#.#
|..O..#O..O
|.......O..
|#....###..
|#OO..#....""".stripMargin
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 updateColumnAsString(matrix: Map[(Int, Int), Char],
columnNumber: Int,
updater: (String) => String): Map[(Int, Int), Char] =
val column = matrix.collect {
case ((row, col), ch) if col == columnNumber => (row, ch)
}.toList.sortBy(_._1).map(_._2).mkString
val updatedColumn = updater(column)
updatedColumn.zipWithIndex.foldLeft(matrix) { case (matrix, (ch, idx)) =>
matrix.updated((idx, columnNumber), ch)
}
def tiltUp(column: String): String =
column
.split('#')
.map(_.sorted.reverse)
.mkString("#")
def totalLoad(matrix: Map[(Int, Int), Char]): Long =
val maxRow = matrix.keys.maxBy(_._1)._1
matrix.map {
case ((row, col), 'O') => maxRow + 1 - row
case ((row, col), _) => 0
}.sum
@main
def day14part1(): Unit =
val matrix = parseInput(Source.fromResource("day14.txt"))
val maxColumn = matrix.keys.maxBy(_._2)._2
0.to(maxColumn).foldLeft(matrix) {(matrix, c) =>
updateColumnAsString(matrix, c, tiltUp)
}.pipe(totalLoad)
.pipe(println)
}
מחשבות על חלק 2
❤1👍1
החלק השני לא נראה מסובך אבל בגלל שטויות של אינדקסים הוא לקח לי הרבה יותר זמן ממה שהיה צריך. המשימה הפעם היא לסובב את הלוח לכל 4 הכיוונים ואז להמשיך לעשות את זה שוב ושוב סך הכל 1000000000 פעמים (אני אפילו לא יודע איך לקרוא את המספר הזה), ואז לחשב את סכום העומס של הסלעים.
יש פה שני אתגרים - קודם כל צריך לשכפל את הקוד שמטה את הלוח למעלה לכל 4 הכיוונים, ואחרי זה לחשוב איך להריץ אותו המון פעמים ולהגיע מתישהו לתוצאה. פה אפשר לשים לב שבדרך כלל אחרי כמה פעמים שנטה את הלוח לכל הכיוונים הכדורים ייכנסו למסלול קבוע ולכן נרצה למצוא את המעגל הזה ואז לא נצטרך להריץ את הלולאה 1000000000 פעמים אלא רק בכפולה של גודל המעגל.
באתגר הראשון בטח שהיה עדיף לוותר על שכפולים ולהוציא את הפונקציונאליות המשותפת לפונקציות, אבל אנחנו פה באילוצי זמן וממילא לא נחזור לקוד הזה אז נקרא לזה גירסת הקופי פייסט:
בשביל האתגר השני כבר אפשר להסתפק בפונקציה רקורסיבית שמחפשת את המעגל:
בקוד הפיתרון עצמו צריך לזכור שהמעגל כנראה לא מתחיל מהמיקום הראשון, כלומר יהיה לנו כמה פעמים שנטה את הלוח לכל הכיוונים ואז רק הכדורים ייכנסו למסלול מעגלי. ככה נראה הפיתרון בסקאלה:
יש לכם רעיונות אחרים? פיתרונות מעניינים בשפות אחרות? מוזמנים תמיד לשתף בתגובות או בטלגרם.
יש פה שני אתגרים - קודם כל צריך לשכפל את הקוד שמטה את הלוח למעלה לכל 4 הכיוונים, ואחרי זה לחשוב איך להריץ אותו המון פעמים ולהגיע מתישהו לתוצאה. פה אפשר לשים לב שבדרך כלל אחרי כמה פעמים שנטה את הלוח לכל הכיוונים הכדורים ייכנסו למסלול קבוע ולכן נרצה למצוא את המעגל הזה ואז לא נצטרך להריץ את הלולאה 1000000000 פעמים אלא רק בכפולה של גודל המעגל.
באתגר הראשון בטח שהיה עדיף לוותר על שכפולים ולהוציא את הפונקציונאליות המשותפת לפונקציות, אבל אנחנו פה באילוצי זמן וממילא לא נחזור לקוד הזה אז נקרא לזה גירסת הקופי פייסט:
def updateRowAsString(matrix: Map[(Int, Int), Char],
rowNumber: Int,
updater: String => String): Map[(Int, Int), Char] =
val row = matrix.collect {
case ((row, col), ch) if row == rowNumber => (col, ch)
}.toList.sortBy(_._1).map(_._2).mkString
val updated = updater(row)
updated.zipWithIndex.foldLeft(matrix) { case (matrix, (ch, idx)) =>
matrix.updated((rowNumber, idx), ch)
}
def tiltUp(column: String): String =
column
.split('#')
.map(_.sorted.reverse)
.mkString("#")
def tiltDown(column: String): String =
column
.split('#')
.map(_.sorted)
.mkString("#")
def tiltLeft(row: String): String =
row
.split('#')
.map(_.sorted.reverse)
.mkString("#")
def tiltRight(row: String): String =
row
.split('#')
.map(_.sorted)
.mkString("#")
def spinCycle(matrix: Map[(Int, Int), Char]): Map[(Int, Int), Char] =
val maxColumn = matrix.keys.maxBy(_._2)._2
val maxRow = matrix.keys.maxBy(_._1)._1
val s1 = 0.to(maxColumn).foldLeft(matrix) { (matrix, c) => updateColumnAsString(matrix, c, tiltUp) }
val s2 = 0.to(maxRow).foldLeft(s1) { (matrix, c) => updateRowAsString(matrix, c, tiltLeft) }
val s3 = 0.to(maxColumn).foldLeft(s2) { (matrix, c) => updateColumnAsString(matrix, c, tiltDown) }
val s4 = 0.to(maxRow).foldLeft(s3) { (matrix, c) => updateRowAsString(matrix, c, tiltRight) }
s4
בשביל האתגר השני כבר אפשר להסתפק בפונקציה רקורסיבית שמחפשת את המעגל:
@tailrec
def findCycle(matrix: Map[(Int, Int), Char],
iterate: (Map[(Int, Int), Char] => Map[(Int, Int), Char]),
seen: List[Map[(Int, Int), Char]] = List()): List[Map[(Int, Int), Char]] =
if (seen.contains(matrix)) {
seen :+ matrix
} else {
findCycle(iterate(matrix), iterate, seen :+ matrix)
}
בקוד הפיתרון עצמו צריך לזכור שהמעגל כנראה לא מתחיל מהמיקום הראשון, כלומר יהיה לנו כמה פעמים שנטה את הלוח לכל הכיוונים ואז רק הכדורים ייכנסו למסלול מעגלי. ככה נראה הפיתרון בסקאלה:
@main
def day14part2(): Unit =
val matrix = parseInput(Source.fromResource("day14.txt"))
val maxColumn = matrix.keys.maxBy(_._2)._2
val maxRow = matrix.keys.maxBy(_._1)._1
val cycle :+ dup = findCycle(matrix, spinCycle)
val prefix = cycle.indexOf(dup)
val actualCycle = cycle.drop(prefix)
LazyList.iterate(matrix)(spinCycle).take(prefix + ((1000000000 - prefix) % actualCycle.size) + 1)
.last
.pipe(totalLoad)
.pipe(println)
יש לכם רעיונות אחרים? פיתרונות מעניינים בשפות אחרות? מוזמנים תמיד לשתף בתגובות או בטלגרם.
היום למדתי לא לסמוך על הקומפיילר של סקאלה
אין דבר יותר מתסכל מלבנות על הקומפיילר שיזהה בשבילך שגיאות בטיפוסים ולגלות שגם לו אין מושג. היום למדתי שבסקאלה זה יכול לקרות בקלות באזורים של העבודה המשותפת בין סקאלה ו Java. הנה דוגמה קצרה וטפשית שחבל שאף אחד לא סיפר לי עליה כשהתחלתי ללמוד סקאלה -
התוצאה היא 0, למרות ש a נמצא במפה. אפשר לטעון ששני הפרמטרים האחרים בלבלו אותו אבל האמת שגם אם נעביר את המחרוזת a לכל הפרמטרים נקבל את אותו 0. הבעיה האמיתית כאן היא ש get של java.util.Map מצפה לקבל פרמטר אחד. העברת יותר פרמטרים לפונקציה היא טעות שהיתה צריכה להתגלות בזמן קומפילציה.
צריך להגיד - מה שמתסכל בסיפור הזה הוא שהקומפיילר כן מזהה טעויות דומות אחרות, למשל הקוד הזה לא מתקמפל כי מנסים להעביר מספר פרמטרים לא נכון לפונקציה של Java:
אבל כשקומפיילר תופס בעיות רק ב 99% מהמקרים זה לפעמים יותר מתסכל מאשר שלא יתפוס בעיות בכלל.
אין דבר יותר מתסכל מלבנות על הקומפיילר שיזהה בשבילך שגיאות בטיפוסים ולגלות שגם לו אין מושג. היום למדתי שבסקאלה זה יכול לקרות בקלות באזורים של העבודה המשותפת בין סקאלה ו Java. הנה דוגמה קצרה וטפשית שחבל שאף אחד לא סיפר לי עליה כשהתחלתי ללמוד סקאלה -
import scala.jdk.CollectionConverters._
val m = Map("a" -> 10)
m.asJava.get("a", 2, 3)
התוצאה היא 0, למרות ש a נמצא במפה. אפשר לטעון ששני הפרמטרים האחרים בלבלו אותו אבל האמת שגם אם נעביר את המחרוזת a לכל הפרמטרים נקבל את אותו 0. הבעיה האמיתית כאן היא ש get של java.util.Map מצפה לקבל פרמטר אחד. העברת יותר פרמטרים לפונקציה היא טעות שהיתה צריכה להתגלות בזמן קומפילציה.
צריך להגיד - מה שמתסכל בסיפור הזה הוא שהקומפיילר כן מזהה טעויות דומות אחרות, למשל הקוד הזה לא מתקמפל כי מנסים להעביר מספר פרמטרים לא נכון לפונקציה של Java:
val m = Map("a" -> 10)
m.asJava.getOrDefault("a", 1, 2)
-- Error: ----------------------------------------------------------------------
2 |m.asJava.getOrDefault("a", 1, 2)
| ^
|too many arguments for method getOrDefault in trait Map: (x$0: Object, x$1: V): V
1 error found
אבל כשקומפיילר תופס בעיות רק ב 99% מהמקרים זה לפעמים יותר מתסכל מאשר שלא יתפוס בעיות בכלל.
טכניקה וקצב
כשהטכניקה טובה אין בעיה לעבוד לאט. הקצב יגיע בקצב שלו. אבל כשהקצב מהיר מאוד קשה לשים לב לפרטים ולשפר את הטכניקה.
בקוד מאוד קל לראות את זה - מתכנתים רצים כדי להספיק את הדדליין ומתרגלים לעבוד בקצב מהיר מדי עבורם, ולא משנה כמה יותר מהר יכתבו את הפיצ'רים לאורך זמן זה תמיד יהיה יותר לאט.
קל ליפול למעגל הזה. פעם אחת דילוורת פיצ'ר יחסית מהר וכולם היו שמחים, ואולי באותו רגע לא שמת לב לבעיות שאותו פיצ'ר יצר. פעם שניה כבר מצפים ממך לאותו קצב, ועם הזמן הציפיה מסביב ומעצמך עובדת לרעתך.
המפתח החוצה זו הטכניקה. ככל שנקפיד לעבוד על טכניקה טובה יותר לפיתוח (בדיקות אוטומטיות, תיעוד, ארגון מחדש של הקוד במקום העתקה, פיתרון בעיות אמיתיות במקום אבסטרקציות מיותרות, ביצועים), כך נגלה שקצב העבודה יורד. וזה מצוין, כי כשהטכניקה טובה הקצב יבוא בקצב שלו.
כשהטכניקה טובה אין בעיה לעבוד לאט. הקצב יגיע בקצב שלו. אבל כשהקצב מהיר מאוד קשה לשים לב לפרטים ולשפר את הטכניקה.
בקוד מאוד קל לראות את זה - מתכנתים רצים כדי להספיק את הדדליין ומתרגלים לעבוד בקצב מהיר מדי עבורם, ולא משנה כמה יותר מהר יכתבו את הפיצ'רים לאורך זמן זה תמיד יהיה יותר לאט.
קל ליפול למעגל הזה. פעם אחת דילוורת פיצ'ר יחסית מהר וכולם היו שמחים, ואולי באותו רגע לא שמת לב לבעיות שאותו פיצ'ר יצר. פעם שניה כבר מצפים ממך לאותו קצב, ועם הזמן הציפיה מסביב ומעצמך עובדת לרעתך.
המפתח החוצה זו הטכניקה. ככל שנקפיד לעבוד על טכניקה טובה יותר לפיתוח (בדיקות אוטומטיות, תיעוד, ארגון מחדש של הקוד במקום העתקה, פיתרון בעיות אמיתיות במקום אבסטרקציות מיותרות, ביצועים), כך נגלה שקצב העבודה יורד. וזה מצוין, כי כשהטכניקה טובה הקצב יבוא בקצב שלו.
👍4
מחפש לומדי שפות אמיצים לפרויקט חדש
הי חברים,
חלק מכם יודעים כבר שחוץ מלכתוב על שפות תכנות אני גם אוהב ללמוד שפות מדוברות (כן רגילות של אנשים), ואחד הקשיים בלימוד שפה הוא שיפור אוצר המילים. אין בעיה למצוא מילים חדשות, הן שם בכל ספר, סידרה או פוסט שקוראים בשפה החדשה. גם אין בעיה למצוא את התרגום שלהן והיום בלחיצת כפתור מתוך הדפדפן אפשר לראות תרגום של כל מילה ואפילו של עמודים שלמים. אבל כשאנחנו מגיעים לכתוב או לדבר אנחנו מגלים שאותן מילים יפות שמצאנו ברשת פשוט נעלמו לנו מהמוח.
וזה גם הגיוני. המוח שלנו לא אוהב לבזבז אנרגיה בלשמור מידע שהוא לא צריך, ולכן אם אנחנו רואים מילה שוב ושוב בהקשרים שונים לא תהיה לנו בעיה להשתמש בה, אבל אם נתקלנו במילה פעם אחת באיזה פוסט אז היא לא נחשבת מספיק חשובה וייקח למוח יותר זמן להיזכר בה.
מה עושים? את הפיתרון לא אני המצאתי והוא נקרא Spaced Repetition. בגדול אנחנו גורמים למוח לחשוב שמילה היא חשובה כדי שהמוח ייצור עבורה את האינדקס המתאים ויהיה קל להיזכר בה כשנצטרך. הבעיה שלמרות שניסיתי המון אפליקציות מהסוג הזה אף אחת לא עבדה כמו שצריך ובסוף נטשתי את כולן. בשביל לנסות להוכיח שהבעיה לא בי אלא באפליקציות התחלתי לכתוב מערכת משלי לשינון מילים ובינתיים היא עובדת לי לא רע, וכאן אנחנו מגיעים לקריאה לפעולה של הפוסט הזה - אני מחפש להכניס עוד משתמשים למערכת כדי לקבל יותר פידבק ולעזור לי לראות את הדברים שעדיין צריכים שיפור.
לכן אם אתם-
1. דוברי עברית שרוצים לשפר את האנגלית שלכם, או דוברי אנגלית שרוצים לשפר את הספרדית או הצרפתית שלכם.
2. לומדים באופן פעיל את השפה (קוראים, רואים סרטים, מדברים) ונתקלים באופן שוטף במילים חדשות שהייתם רוצים לזכור.
3. שמחים לנסות מוצרים חדשים לפני שהם מוכנים ולשתף פידבק מועיל כדי לעזור לשפר.
בבקשה השאירו לי הודעה כאן באתר או למייל ynon שטרודל tocode.co.il ואכניס אתכם לקבוצת הניסוי.
הי חברים,
חלק מכם יודעים כבר שחוץ מלכתוב על שפות תכנות אני גם אוהב ללמוד שפות מדוברות (כן רגילות של אנשים), ואחד הקשיים בלימוד שפה הוא שיפור אוצר המילים. אין בעיה למצוא מילים חדשות, הן שם בכל ספר, סידרה או פוסט שקוראים בשפה החדשה. גם אין בעיה למצוא את התרגום שלהן והיום בלחיצת כפתור מתוך הדפדפן אפשר לראות תרגום של כל מילה ואפילו של עמודים שלמים. אבל כשאנחנו מגיעים לכתוב או לדבר אנחנו מגלים שאותן מילים יפות שמצאנו ברשת פשוט נעלמו לנו מהמוח.
וזה גם הגיוני. המוח שלנו לא אוהב לבזבז אנרגיה בלשמור מידע שהוא לא צריך, ולכן אם אנחנו רואים מילה שוב ושוב בהקשרים שונים לא תהיה לנו בעיה להשתמש בה, אבל אם נתקלנו במילה פעם אחת באיזה פוסט אז היא לא נחשבת מספיק חשובה וייקח למוח יותר זמן להיזכר בה.
מה עושים? את הפיתרון לא אני המצאתי והוא נקרא Spaced Repetition. בגדול אנחנו גורמים למוח לחשוב שמילה היא חשובה כדי שהמוח ייצור עבורה את האינדקס המתאים ויהיה קל להיזכר בה כשנצטרך. הבעיה שלמרות שניסיתי המון אפליקציות מהסוג הזה אף אחת לא עבדה כמו שצריך ובסוף נטשתי את כולן. בשביל לנסות להוכיח שהבעיה לא בי אלא באפליקציות התחלתי לכתוב מערכת משלי לשינון מילים ובינתיים היא עובדת לי לא רע, וכאן אנחנו מגיעים לקריאה לפעולה של הפוסט הזה - אני מחפש להכניס עוד משתמשים למערכת כדי לקבל יותר פידבק ולעזור לי לראות את הדברים שעדיין צריכים שיפור.
לכן אם אתם-
1. דוברי עברית שרוצים לשפר את האנגלית שלכם, או דוברי אנגלית שרוצים לשפר את הספרדית או הצרפתית שלכם.
2. לומדים באופן פעיל את השפה (קוראים, רואים סרטים, מדברים) ונתקלים באופן שוטף במילים חדשות שהייתם רוצים לזכור.
3. שמחים לנסות מוצרים חדשים לפני שהם מוכנים ולשתף פידבק מועיל כדי לעזור לשפר.
בבקשה השאירו לי הודעה כאן באתר או למייל ynon שטרודל tocode.co.il ואכניס אתכם לקבוצת הניסוי.
Wikipedia
Spaced repetition
evidence-based learning technique performed with flashcards
👍5🏆2
טרייד אוף לא הגיוני
וויז החליטו שאני צריך לנסוע מרחק כפול (16 ק"מ במקום 8) כדי לחסוך 3 דקות בנסיעה. האלגוריתם לא חשב שאולי הוא טועה וגם בדרך הארוכה עשוי להיות פקק, ובפועל אני רק מגדיל את הסיכון.
במקום אחר, lcl.host החליטו שיהיה להם יותר קל אם רק קלאיינטים בגירסה הכי חדשה יוכלו להתחבר. הם לא חשבו שאולי הם לא יוכלו להוציא גירסה חדשה באותו זמן לכל דרכי ההפצה וכך אנשים נתקעו בלי יכולת להשתמש בתוכנה (יש גירסה חדשה זמינה אבל עדיין לא בשביל המחשב שלך).
בעולם ה JavaScript אנחנו רגילים לראות התפוצצות של גודל הדפים בגלל שכולם משתמשים בפריימוורקים ומוסיפים תלויות כאילו אין מחר. בסוף כל הקוד הזה צריך לרוץ אצל מישהו על המכונה וגורם לאיטיות בטעינה ובזמן הריצה.
״כמה זה עולה?״ זאת תמיד שאלה חשובה כי רק דרכה אפשר להבין מה הטרייד אוף שעל הפרק ולחשוב אם הוא מתאים לאתגר שלנו.
וויז החליטו שאני צריך לנסוע מרחק כפול (16 ק"מ במקום 8) כדי לחסוך 3 דקות בנסיעה. האלגוריתם לא חשב שאולי הוא טועה וגם בדרך הארוכה עשוי להיות פקק, ובפועל אני רק מגדיל את הסיכון.
במקום אחר, lcl.host החליטו שיהיה להם יותר קל אם רק קלאיינטים בגירסה הכי חדשה יוכלו להתחבר. הם לא חשבו שאולי הם לא יוכלו להוציא גירסה חדשה באותו זמן לכל דרכי ההפצה וכך אנשים נתקעו בלי יכולת להשתמש בתוכנה (יש גירסה חדשה זמינה אבל עדיין לא בשביל המחשב שלך).
בעולם ה JavaScript אנחנו רגילים לראות התפוצצות של גודל הדפים בגלל שכולם משתמשים בפריימוורקים ומוסיפים תלויות כאילו אין מחר. בסוף כל הקוד הזה צריך לרוץ אצל מישהו על המכונה וגורם לאיטיות בטעינה ובזמן הריצה.
״כמה זה עולה?״ זאת תמיד שאלה חשובה כי רק דרכה אפשר להבין מה הטרייד אוף שעל הפרק ולחשוב אם הוא מתאים לאתגר שלנו.
🔥5
היום למדתי: scope ב CSS
אני אוהב איך ש CSS מתפתח ומצליח לפתור את הבעיות תמיד שלוש שנים אחרי הזמן. הסיפור היום הוא על פיצ'ר מ CSS Selectors 4 שנקרא scope שממש הייתי צריך כל החיים אבל בכל מקרה אנחנו לא פה בשביל להתלונן וטוב מאוחר מלעולם לא.
מה ש scope עושה זה נותן לנו להגדיר כללי CSS שמשפיעים רק על אזור מסוים. כלומר אם יש לנו את ה HTML הזה (מתוך הדוגמה ב MDN):
ואני רוצה לצבוע כל אחד מהדיבים והלינקים שאיתו בצבעים שונים תמיד יכלתי לכתוב CSS בסגנון הזה:
די הרבה זמן אני כבר יכול להשתמש ב CSS כזה:
ולאחרונה אני יכול להשתמש גם בכתיב הזה כדי להגיע לאותה תוצאה:
ההבדל בין שני האחרונים קשור למשמעות הסמנטית שלהם. האמצעי בעל אותה משמעות כמו הראשון וה Specificity של ההורה נלקח בחשבון כשמחשבים את הערך (צבע במקרה שלנו). ב scope ההורה הוא לא חלק משורת ה Selector ולכן לא נלקח בחשבון בפיתרון קונפליקטים. דוגמה? בטח. ה CSS הזה:
נותן לאלמנטי ה a צבע צהוב, למרות שבתוך הסקופ מוגדר ערך אחר. הערך העליון הוא יותר ספציפי ולכן מנצח. לעומתו ה CSS הזה:
ייתן ללינקים שבתוך ה light-scheme את הצבע darkmagenta בגלל שהסלקטור שלו יותר ספציפי.
אני אוהב איך ש CSS מתפתח ומצליח לפתור את הבעיות תמיד שלוש שנים אחרי הזמן. הסיפור היום הוא על פיצ'ר מ CSS Selectors 4 שנקרא scope שממש הייתי צריך כל החיים אבל בכל מקרה אנחנו לא פה בשביל להתלונן וטוב מאוחר מלעולם לא.
מה ש scope עושה זה נותן לנו להגדיר כללי CSS שמשפיעים רק על אזור מסוים. כלומר אם יש לנו את ה HTML הזה (מתוך הדוגמה ב MDN):
<div class="light-scheme">
<p>
MDN contains lots of information about
<a href="/en-US/docs/Web/HTML">HTML</a>,
<a href="/en-US/docs/Web/CSS">CSS</a>, and
<a href="/en-US/docs/Web/JavaScript">JavaScript</a>.
</p>
</div>
<div class="dark-scheme">
<p>
MDN contains lots of information about
<a href="/en-US/docs/Web/HTML">HTML</a>,
<a href="/en-US/docs/Web/CSS">CSS</a>, and
<a href="/en-US/docs/Web/JavaScript">JavaScript</a>.
</p>
</div>
ואני רוצה לצבוע כל אחד מהדיבים והלינקים שאיתו בצבעים שונים תמיד יכלתי לכתוב CSS בסגנון הזה:
.light-scheme {
background-color: plum;
}
.light-scheme a {
color: darkmagenta;
}
.dark-scheme {
background-color: darkmagenta;
color: antiquewhite;
}
.dark-scheme a {
color: plum;
}
די הרבה זמן אני כבר יכול להשתמש ב CSS כזה:
.light-scheme {
background-color: plum;
a {
color: darkmagenta;
}
}
.dark-scheme {
background-color: darkmagenta;
color: antiquewhite;
a {
color: plum;
}
}
ולאחרונה אני יכול להשתמש גם בכתיב הזה כדי להגיע לאותה תוצאה:
@scope (.light-scheme) {
:scope {
background-color: plum;
}
a {
color: darkmagenta;
}
}
@scope (.dark-scheme) {
:scope {
background-color: darkmagenta;
color: antiquewhite;
}
a {
color: plum;
}
}
ההבדל בין שני האחרונים קשור למשמעות הסמנטית שלהם. האמצעי בעל אותה משמעות כמו הראשון וה Specificity של ההורה נלקח בחשבון כשמחשבים את הערך (צבע במקרה שלנו). ב scope ההורה הוא לא חלק משורת ה Selector ולכן לא נלקח בחשבון בפיתרון קונפליקטים. דוגמה? בטח. ה CSS הזה:
.light-scheme a {
color: yellow;
}
@scope (.light-scheme) {
:scope {
background-color: plum;
}
a {
color: darkmagenta;
}
}
נותן לאלמנטי ה a צבע צהוב, למרות שבתוך הסקופ מוגדר ערך אחר. הערך העליון הוא יותר ספציפי ולכן מנצח. לעומתו ה CSS הזה:
div {
padding: 10px;
}
.light-scheme a {
color: yellow;
}
.light-scheme {
background-color: plum;
a {
color: darkmagenta;
}
}
ייתן ללינקים שבתוך ה light-scheme את הצבע darkmagenta בגלל שהסלקטור שלו יותר ספציפי.
MDN Web Docs
:scope - CSS | MDN
The :scope CSS pseudo-class represents elements that are a reference point, or scope, for selectors to match against.
👍3👏1
פיתרון Advent Of Code יום 15 בסקאלה - איזה כיף שהמציאו את ListMap
התרגיל של יום 15 ב Advent Of Code היה בגדול ממש קל, במיוחד בהשוואה לימים הקודמים עם המטריצות. בפוסט הפעם לא אכתוב את הסיפור המלא (בעיקר כי הוא עמוס בחישובים לא מעניינים) ונתמקד בחלק היחיד שכן היה מעניין ביום הזה - סידור העדשות בקופסאות.
האתגר
נדמיין 256 קופסאות, בכל קופסה אפשר לשים עדשות ולכל עדשה יש תווית (טקסט) וערך (מספר). אנחנו מקבלים בתור דף הוראות רשימה של תוויות וערכים וצריכים לשים את העדשות בתוך הקופסאות לפי הכלל הבא:
1. אם התווית כבר נמצאת בקופסה, יש להחליף את הערך.
2. אם התווית לא בקופסה יש להוסיף אותה בתור העדשה האחרונה של הקופסה.
3. אם לא עבר ערך יש להוציא את העדשה עם התווית מהקופסה.
האתגר הוא לשמור על סדר ההכנסה בתוך כל קופסה כי בסוף השאלה היא לחשב איזשהו סכום שמערב את האינדקס של כל עדשה בקופסה.
קלט לדוגמה נראה כך:
ויש גם פונקציה שמקבלת תווית ומחזירה את מספר הקופסה שלתוכה צריך להכניס את העדשה שזו התווית שלה. הפיתרון עבור קלט הדוגמה הוא:
פיתרון כללי
מבנה הנתונים המרכזי שעזר לי לפתור את התרגיל הזה נקרא בסקאלה ListMap (הוא קיים גם בפייתון ושם נקרא OrderedDict. זה מבנה נתונים שהוא שילוב בין מילון לרשימה - מכניסים אליו דברים לפי מפתחות אבל הוא שומר על סדר ההכנסה שלהם. בסקאלה שימוש לדוגמה בו נראה כך:
כלומר כל שינוי במילון ישמור תמיד על סדר המפתחות, ויש גם פונקציות לאיטרציה על המילון שתמיד יעבדו לפי סדר ההכנסה. אם ערך במפתח מסוים עודכן הוא עדיין ישמור על המיקום שלו לפי סדר ההכנסה המקורי.
וזה מצוין כי עכשיו אפשר לתרגם את משימת העדשות לפעולות על מילון-רשימה:
1. בכל קופסה נשים ListMap.
2. כל פקודה על עדשה (הכנסה, שינוי או הוצאה) תתורגם לפעולה על אותו ListMap.
3. בסוף יהיו לנו את כל המפתחות לפי סדר ההכנסה.
קוד? זה החלק הרלוונטי מתוך התוכנית:
לוקחים הוראה, מפצלים אותה לפי הפסיק או המינוס, מגלים את מספר הקופסה שמתאים לה ומעדכנים את הקופסה המתאימה.
לסקרנים קוד התוכנית המלא עם כל החישובים הפחות מעניינים מהתרגיל הוא:
התרגיל של יום 15 ב Advent Of Code היה בגדול ממש קל, במיוחד בהשוואה לימים הקודמים עם המטריצות. בפוסט הפעם לא אכתוב את הסיפור המלא (בעיקר כי הוא עמוס בחישובים לא מעניינים) ונתמקד בחלק היחיד שכן היה מעניין ביום הזה - סידור העדשות בקופסאות.
האתגר
נדמיין 256 קופסאות, בכל קופסה אפשר לשים עדשות ולכל עדשה יש תווית (טקסט) וערך (מספר). אנחנו מקבלים בתור דף הוראות רשימה של תוויות וערכים וצריכים לשים את העדשות בתוך הקופסאות לפי הכלל הבא:
1. אם התווית כבר נמצאת בקופסה, יש להחליף את הערך.
2. אם התווית לא בקופסה יש להוסיף אותה בתור העדשה האחרונה של הקופסה.
3. אם לא עבר ערך יש להוציא את העדשה עם התווית מהקופסה.
האתגר הוא לשמור על סדר ההכנסה בתוך כל קופסה כי בסוף השאלה היא לחשב איזשהו סכום שמערב את האינדקס של כל עדשה בקופסה.
קלט לדוגמה נראה כך:
rn=1,cm-,qp=3,cm=2,qp-,pc=4,ot=9,ab=5,pc-,pc=6,ot=7
ויש גם פונקציה שמקבלת תווית ומחזירה את מספר הקופסה שלתוכה צריך להכניס את העדשה שזו התווית שלה. הפיתרון עבור קלט הדוגמה הוא:
After "rn=1":
Box 0: [rn 1]
After "cm-":
Box 0: [rn 1]
After "qp=3":
Box 0: [rn 1]
Box 1: [qp 3]
After "cm=2":
Box 0: [rn 1] [cm 2]
Box 1: [qp 3]
After "qp-":
Box 0: [rn 1] [cm 2]
After "pc=4":
Box 0: [rn 1] [cm 2]
Box 3: [pc 4]
After "ot=9":
Box 0: [rn 1] [cm 2]
Box 3: [pc 4] [ot 9]
After "ab=5":
Box 0: [rn 1] [cm 2]
Box 3: [pc 4] [ot 9] [ab 5]
After "pc-":
Box 0: [rn 1] [cm 2]
Box 3: [ot 9] [ab 5]
After "pc=6":
Box 0: [rn 1] [cm 2]
Box 3: [ot 9] [ab 5] [pc 6]
After "ot=7":
Box 0: [rn 1] [cm 2]
Box 3: [ot 7] [ab 5] [pc 6]
פיתרון כללי
מבנה הנתונים המרכזי שעזר לי לפתור את התרגיל הזה נקרא בסקאלה ListMap (הוא קיים גם בפייתון ושם נקרא OrderedDict. זה מבנה נתונים שהוא שילוב בין מילון לרשימה - מכניסים אליו דברים לפי מפתחות אבל הוא שומר על סדר ההכנסה שלהם. בסקאלה שימוש לדוגמה בו נראה כך:
scala> ListMap("foo" -> 10, "bar" -> 20, "buz" -> 30)
val res2: scala.collection.immutable.ListMap[String, Int] = ListMap(foo -> 10, bar -> 20, buz -> 30)
scala> ListMap("foo" -> 10, "bar" -> 20, "buz" -> 30).updated("bar", 40)
val res3: scala.collection.immutable.ListMap[String, Int] = ListMap(foo -> 10, bar -> 40, buz -> 30)
scala> ListMap("foo" -> 10, "bar" -> 20, "buz" -> 30).updated("foo", 30)
val res4: scala.collection.immutable.ListMap[String, Int] = ListMap(foo -> 30, bar -> 20, buz -> 30)
scala> ListMap("foo" -> 10, "bar" -> 20, "buz" -> 30).updated("hiz", 50)
val res5: scala.collection.immutable.ListMap[String, Int] = ListMap(foo -> 10, bar -> 20, buz -> 30, hiz -> 50)
כלומר כל שינוי במילון ישמור תמיד על סדר המפתחות, ויש גם פונקציות לאיטרציה על המילון שתמיד יעבדו לפי סדר ההכנסה. אם ערך במפתח מסוים עודכן הוא עדיין ישמור על המיקום שלו לפי סדר ההכנסה המקורי.
וזה מצוין כי עכשיו אפשר לתרגם את משימת העדשות לפעולות על מילון-רשימה:
1. בכל קופסה נשים ListMap.
2. כל פקודה על עדשה (הכנסה, שינוי או הוצאה) תתורגם לפעולה על אותו ListMap.
3. בסוף יהיו לנו את כל המפתחות לפי סדר ההכנסה.
קוד? זה החלק הרלוונטי מתוך התוכנית:
.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
}
}
לוקחים הוראה, מפצלים אותה לפי הפסיק או המינוס, מגלים את מספר הקופסה שמתאים לה ומעדכנים את הקופסה המתאימה.
לסקרנים קוד התוכנית המלא עם כל החישובים הפחות מעניינים מהתרגיל הוא:
import scala.io.Source
Python documentation
collections — Container datatypes
Source code: Lib/collections/__init__.py This module implements specialized container datatypes providing alternatives to Python’s general purpose built-in containers, dict, list, set, and tuple.,,...