הג'וניור של אתמול
הג'וניור של אתמול קיבל מפרט טכני מדויק מהארכיטקט או המנהל והיה צריך לבנות אותו. הג'וניור של מחר יצטרך לבוא לארכיטקט עם המפרט הטכני ונתונים שמוכיחים למה הבחירה שלו הכי מתאימה למצב.
הג'וניור של אתמול קיבל מהפרודקט מסמך אפיון ועיצוב לפיצ'ר והיה צריך לבנות אותו למערכת. הג'וניור של מחר יצטרך להבין למה ה AI לא הצליח לבנות את הפיצ'ר שאנשי הפרודקט ביקשו ולעדכן את הפרויקט והתשתית כדי שאפשר יהיה להתקדם.
הג'וניור של אתמול כתב בדיקות אוטומטיות כדי ללמוד על המערכת ולהבין איך דברים מתחברים. הג'וניור של מחר יכתוב תשתית של בדיקות וייתן למכונה לממש 100 בדיקות בזמן שהוא לומד את הקוד.
הג'וניור של אתמול פתר באגים קטנים שלאף אחד לא היה זמן לחקור. הג'וניור של מחר יבנה תשתית שמזהה ומתקנת את הבאגים האלה בצורה אוטומטית בלי שאף אחד יצטרך לחקור.
ה AI לא מחליף את הג'וניורים הוא מחליף את מה שהם צריכים לעשות. ברוכים הבאים לתעשייה.
הג'וניור של אתמול קיבל מפרט טכני מדויק מהארכיטקט או המנהל והיה צריך לבנות אותו. הג'וניור של מחר יצטרך לבוא לארכיטקט עם המפרט הטכני ונתונים שמוכיחים למה הבחירה שלו הכי מתאימה למצב.
הג'וניור של אתמול קיבל מהפרודקט מסמך אפיון ועיצוב לפיצ'ר והיה צריך לבנות אותו למערכת. הג'וניור של מחר יצטרך להבין למה ה AI לא הצליח לבנות את הפיצ'ר שאנשי הפרודקט ביקשו ולעדכן את הפרויקט והתשתית כדי שאפשר יהיה להתקדם.
הג'וניור של אתמול כתב בדיקות אוטומטיות כדי ללמוד על המערכת ולהבין איך דברים מתחברים. הג'וניור של מחר יכתוב תשתית של בדיקות וייתן למכונה לממש 100 בדיקות בזמן שהוא לומד את הקוד.
הג'וניור של אתמול פתר באגים קטנים שלאף אחד לא היה זמן לחקור. הג'וניור של מחר יבנה תשתית שמזהה ומתקנת את הבאגים האלה בצורה אוטומטית בלי שאף אחד יצטרך לחקור.
ה AI לא מחליף את הג'וניורים הוא מחליף את מה שהם צריכים לעשות. ברוכים הבאים לתעשייה.
פתרון Advent Of Code 2025 יום 6
אני ממשיך עם סדרת Advent Of Code לשמחתנו השנה יש רק 12 חידות אז יש סיכוי שאצליח לפתור ולפרסם את כולם לפני שיעלו האתגרים של 2026. אנחנו היום באמצע הדרך עם יום 6 ותרגיל שאני חשבתי שהיה סופר מעניין. בואו נראה את המספרים.
האתגר
נתון קלט שנראה די פשוט:
יש פה טורים של מספרים ובתחתית כל טור מופיע סימן פלוס או כפול. המשימה שלנו לכפול או לחבר את המספרים בטור (לפי הסימן) ובסוף לחבר את התוצאות. בדוגמה התוצאה היא 4277556.
הפתרון
הדרך לפתרון די פשוטה: קוראים את הקלט שורה שורה, שוברים כל שורה לפי רווחים, כי אנחנו יודעים שיש את אותו מספר עמודות בכל שורה ובונים מערך דו מימדי של כל הגריד. בסוף לוקחים את הסימן האחרון בכל עמודה בתור פעולה ומפעילים אותה על כל הפריטים באותה עמודה. זה הקוד ברובי:
הטוויסט
אחרי שמגישים את החלק הראשון מגלים את הטוויסט בתרגיל - שימו לב אומרים לנו שחלק מהמספרים מיושרים לימין וחלק לשמאל:
בעצם המספרים בכל טור כתובים מלמעלה למטה ולא רגיל משמאל לימין, לכן בטור הימני המספר הראשון הוא 4 (הטור הימני ביותר), אחריו יש לנו טור בו יש את 4, 3 ו-1 ולכן המספר הוא 431 והמספר האחרון בטור הימני הוא 623. חיבור שלושתם נותן 1058.
השינוי הזה משפיע ישירות על הבלוק:
בחלק הראשון חשבנו שאפשר להתעלם מרווחים ושיש אותו מספר של עמודות בכל שורה. עכשיו אנחנו מגלים שעדיין יש אותו מספר עמודות בכל שורה אבל חשוב לשמור על הריווח כי 64 שמיושר לשמאל לא מתנהג כמו 51 שמיושר לימין שיושב ממש משמאלו.
האתגר השני שלא רואים בנתוני הדוגמה הוא שכל עמודה יכולה להיות באורך שונה. בנתוני הדוגמה כל העמודות כללו מספרים בני 3 ספרות ולכן לכולן היה אותו אורך אבל במקרה הכללי קל לראות שאם יש טור עם מספרים בני שתי ספרות או ספרה אחת נדרש טיפול שונה, כלומר קלט כזה לגמרי יכול לקרות:
מה עושים? הפעם אנחנו צריכים לזהות מה אורך כל עמודה ואז לפצל את השורה לעמודות בצורה קשיחה לפי מספר התווים שאנחנו יודעים שיש בכל עמודה. אפשר להגיד שבחלק הקודם שברנו את השורה לפי תו הפרדה ועכשיו אנחנו שוברים לפי אורך או אינדקסים. בשביל לגלות את האורך של כל עמודה אפשר להסתכל על השורה האחרונה שורת הפעולות. זה הקוד ברובי לחלק השני:
סך הכל אני מודה שבחלק השני הסתבכתי קצת עם האינדקסים וכנראה אפשר לראות את זה כשקוראים את הקוד. למרות זאת אחרי כמה בעיטות הקוד עבד ומצליח לטפל בכל מקרי הקצה שמצאתי ולהגיע לתשובה הנכונה.
אני ממשיך עם סדרת Advent Of Code לשמחתנו השנה יש רק 12 חידות אז יש סיכוי שאצליח לפתור ולפרסם את כולם לפני שיעלו האתגרים של 2026. אנחנו היום באמצע הדרך עם יום 6 ותרגיל שאני חשבתי שהיה סופר מעניין. בואו נראה את המספרים.
האתגר
נתון קלט שנראה די פשוט:
123 328 51 64
45 64 387 23
6 98 215 314
* + * +
יש פה טורים של מספרים ובתחתית כל טור מופיע סימן פלוס או כפול. המשימה שלנו לכפול או לחבר את המספרים בטור (לפי הסימן) ובסוף לחבר את התוצאות. בדוגמה התוצאה היא 4277556.
הפתרון
הדרך לפתרון די פשוטה: קוראים את הקלט שורה שורה, שוברים כל שורה לפי רווחים, כי אנחנו יודעים שיש את אותו מספר עמודות בכל שורה ובונים מערך דו מימדי של כל הגריד. בסוף לוקחים את הסימן האחרון בכל עמודה בתור פעולה ומפעילים אותה על כל הפריטים באותה עמודה. זה הקוד ברובי:
class Day6
def initialize(filename)
@filename = filename
end
def part1
columns = []
File.read(@filename).lines do |line|
columns << line.strip.split(/\s+/)
end
@operators = columns.pop
@values = columns
results = @operators.each_with_index.map do |op, idx|
@values.map {|v| v[idx].to_i }.reduce(op.to_sym)
end
pp results.sum
end
end
הטוויסט
אחרי שמגישים את החלק הראשון מגלים את הטוויסט בתרגיל - שימו לב אומרים לנו שחלק מהמספרים מיושרים לימין וחלק לשמאל:
123 328 51 64
45 64 387 23
6 98 215 314
* + * +
בעצם המספרים בכל טור כתובים מלמעלה למטה ולא רגיל משמאל לימין, לכן בטור הימני המספר הראשון הוא 4 (הטור הימני ביותר), אחריו יש לנו טור בו יש את 4, 3 ו-1 ולכן המספר הוא 431 והמספר האחרון בטור הימני הוא 623. חיבור שלושתם נותן 1058.
השינוי הזה משפיע ישירות על הבלוק:
File.read(@filename).lines do |line|
columns << line.strip.split(/\s+/)
end
בחלק הראשון חשבנו שאפשר להתעלם מרווחים ושיש אותו מספר של עמודות בכל שורה. עכשיו אנחנו מגלים שעדיין יש אותו מספר עמודות בכל שורה אבל חשוב לשמור על הריווח כי 64 שמיושר לשמאל לא מתנהג כמו 51 שמיושר לימין שיושב ממש משמאלו.
האתגר השני שלא רואים בנתוני הדוגמה הוא שכל עמודה יכולה להיות באורך שונה. בנתוני הדוגמה כל העמודות כללו מספרים בני 3 ספרות ולכן לכולן היה אותו אורך אבל במקרה הכללי קל לראות שאם יש טור עם מספרים בני שתי ספרות או ספרה אחת נדרש טיפול שונה, כלומר קלט כזה לגמרי יכול לקרות:
123 32 51 64
45 64 387 23
6 98 215 314
* + * +
מה עושים? הפעם אנחנו צריכים לזהות מה אורך כל עמודה ואז לפצל את השורה לעמודות בצורה קשיחה לפי מספר התווים שאנחנו יודעים שיש בכל עמודה. אפשר להגיד שבחלק הקודם שברנו את השורה לפי תו הפרדה ועכשיו אנחנו שוברים לפי אורך או אינדקסים. בשביל לגלות את האורך של כל עמודה אפשר להסתכל על השורה האחרונה שורת הפעולות. זה הקוד ברובי לחלק השני:
def part2
lines = File.read(@filename).lines
operators_line = lines.pop
column_sizes = operators_line.scan(/[+*]\s*/).map {|i| i.size - 1}
data = lines.map(&:chomp).map do |line|
column_sizes.map {|s| r = line.slice!(0, s); line.slice!(0, 1); r }
end
operators = operators_line.strip.split(/\s+/).map(&:to_sym)
columns = (0...operators.size).map {|col| data.map {|d| d[col] } }
values = columns.map do |column|
(0...column[0].size).map {|i| column.map {|c| c[i] }.join('').to_i }
end
@results = operators.each_with_index.map {|o, i| values[i].reduce(o) }
pp @results.sum
end
סך הכל אני מודה שבחלק השני הסתבכתי קצת עם האינדקסים וכנראה אפשר לראות את זה כשקוראים את הקוד. למרות זאת אחרי כמה בעיטות הקוד עבד ומצליח לטפל בכל מקרי הקצה שמצאתי ולהגיע לתשובה הנכונה.
פתרון Advent Of Code 2025 יום 7
אני רוצה להמשיך בסדרת הפתרונות של Advent Of Code ולדבר על יום 7, יום שלימד אותי שתכנות דינמי זה דווקא די כיף ולא מסובך. כמו אתמול גם פה החלק הראשון פשוט והחלק השני דורש את המאמץ.
האתגר
נתון קלט שמתאר סוג של מסלול מלמעלה למטה:
במסלול יש לנו קרן שמתחילה מהאות S בשורה הראשונה ויורדת כלפי מטה. כל פעם שהיא פוגעת ב
ובסוף נגיע ל:
המשימה היא לחשב כמה פעמים הקרן מתפצלת.
הפתרון
אני מתחיל בהגדרת פונקציית עזר שמוצאת בשורה את כל האינדקסים של הספליטרים:
בגלל שזה רובי אני יכול להגדיר את הפונקציה בתור הרחבה ל Stirng ועכשיו אפשר לכתוב:
כדי להפעיל את הפונקציה על המחרוזת שמשמאל לנקודה. הקוד עצמו בתוספת הערות של AI הוא:
האלגוריתם כמו שאפשר לראות מהקוד הוא בסך הכל פעולות על קבוצות: לוקחים את כל האינדקסים של ה
כששלחתי את הקוד לקלוד עבור Code Review הוא טען שפעולת חיבור של שתי קבוצות ברובי מחזירה מערך אבל בדיקה בקונסול מראה לי שזו טעות וחיבור שתי קבוצות מחזיר קבוצה:
הוא גם טען ששכחתי לעשות require ל set וזו גם טעות שלו. שני הדברים מלמדים אותי שקלוד כנראה חושב שאני בגרסה ישנה יותר של רובי. סך הכל הקוד עבד בסדר גמור והחזיר תוצאה נכונה.
הטוויסט - תכנות דינמי
החלק השני של השאלה הכניס את הטוויסט. דמיינו שבמקום שהקרן תתפצל הזמן עצמו מתפצל ויש לנו עולם אחד בו הקרן הלכה ימינה ועולם אחר בו היא הלכה שמאלה. כל עולם כזה ימשיך בתורו להתפצל לעוד עולמות כשהקרן תמשיך לפגוע בעוד Splitters. זו דוגמה לעולם אפשרי אחד:
וזו דוגמה לעוד עולם אפשרי:
אני רוצה להמשיך בסדרת הפתרונות של Advent Of Code ולדבר על יום 7, יום שלימד אותי שתכנות דינמי זה דווקא די כיף ולא מסובך. כמו אתמול גם פה החלק הראשון פשוט והחלק השני דורש את המאמץ.
האתגר
נתון קלט שמתאר סוג של מסלול מלמעלה למטה:
.......S.......
...............
.......^.......
...............
......^.^......
...............
.....^.^.^.....
...............
....^.^...^....
...............
...^.^...^.^...
...............
..^...^.....^..
...............
.^.^.^.^.^...^.
...............
במסלול יש לנו קרן שמתחילה מהאות S בשורה הראשונה ויורדת כלפי מטה. כל פעם שהיא פוגעת ב
^ היא מתפצלת ל-2 אחת בכל צד של ה ^ לדוגמה אחרי שתי שורות נקבל:.......S.......
.......|.......
......|^|......
...............
......^.^......
...............
.....^.^.^.....
...............
....^.^...^....
...............
...^.^...^.^...
...............
..^...^.....^..
...............
.^.^.^.^.^...^.
...............
ובסוף נגיע ל:
.......S.......
.......|.......
......|^|......
......|.|......
.....|^|^|.....
.....|.|.|.....
....|^|^|^|....
....|.|.|.|....
...|^|^|||^|...
...|.|.|||.|...
..|^|^|||^|^|..
..|.|.|||.|.|..
.|^|||^||.||^|.
.|.|||.||.||.|.
|^|^|^|^|^|||^|
|.|.|.|.|.|||.|
המשימה היא לחשב כמה פעמים הקרן מתפצלת.
הפתרון
אני מתחיל בהגדרת פונקציית עזר שמוצאת בשורה את כל האינדקסים של הספליטרים:
class String
def find_all_indexes(ch)
chars.each_with_index.filter {|c, i| c == ch }.map {|c, i| i }
end
end
בגלל שזה רובי אני יכול להגדיר את הפונקציה בתור הרחבה ל Stirng ועכשיו אפשר לכתוב:
"abcabcabc".find_all_indexes("a")
כדי להפעיל את הפונקציה על המחרוזת שמשמאל לנקודה. הקוד עצמו בתוספת הערות של AI הוא:
def part1
beam_indexes = Set.new
count = 0
File.read(@filename).lines.each do |line|
# Initialize beam positions when we encounter the start marker
if beam_index = line =~ /S/
puts "--- START ---"
beam_indexes = Set.new([beam_index])
end
# Find all splitter positions in current line
splitter_indexes = Set.new(line.find_all_indexes('^'))
# When a beam hits a splitter, it splits into two beams (left and right)
splitted_beams = Set.new((splitter_indexes & beam_indexes).flat_map {|i| [i-1, i+1] })
# Count interactions with splitters
count += (splitter_indexes & beam_indexes).size
# Beams that don't hit splitters continue straight
continuing_beams = beam_indexes - splitter_indexes
# Update beam positions: continuing beams + newly split beams
beam_indexes = continuing_beams + splitted_beams
end
count
end
האלגוריתם כמו שאפשר לראות מהקוד הוא בסך הכל פעולות על קבוצות: לוקחים את כל האינדקסים של ה
^ לקבוצה אחת ואת כל האינדקסים של הקרניים בקבוצה שניה והחיתוך בין שתי הקבוצות הוא בדיוק מספר הפיצולים של אותה שורה. החישוב הוא בשורה:count += (splitter_indexes & beam_indexes).size
כששלחתי את הקוד לקלוד עבור Code Review הוא טען שפעולת חיבור של שתי קבוצות ברובי מחזירה מערך אבל בדיקה בקונסול מראה לי שזו טעות וחיבור שתי קבוצות מחזיר קבוצה:
3.3.5 :005 > (Set.new + Set.new).class
=> Set
הוא גם טען ששכחתי לעשות require ל set וזו גם טעות שלו. שני הדברים מלמדים אותי שקלוד כנראה חושב שאני בגרסה ישנה יותר של רובי. סך הכל הקוד עבד בסדר גמור והחזיר תוצאה נכונה.
הטוויסט - תכנות דינמי
החלק השני של השאלה הכניס את הטוויסט. דמיינו שבמקום שהקרן תתפצל הזמן עצמו מתפצל ויש לנו עולם אחד בו הקרן הלכה ימינה ועולם אחר בו היא הלכה שמאלה. כל עולם כזה ימשיך בתורו להתפצל לעוד עולמות כשהקרן תמשיך לפגוע בעוד Splitters. זו דוגמה לעולם אפשרי אחד:
.......S.......
.......|.......
......|^.......
......|........
.....|^.^......
.....|.........
....|^.^.^.....
....|..........
...|^.^...^....
...|...........
..|^.^...^.^...
..|............
.|^...^.....^..
.|.............
|^.^.^.^.^...^.
|..............
וזו דוגמה לעוד עולם אפשרי:
.......S.......
.......|.......
......|^.......
......|........
......^|^......
.......|.......
.....^|^.^.....
......|........
....^.^|..^....
.......|.......
...^.^.|.^.^...
.......|.......
..^...^|....^..
.......|.......
.^.^.^|^.^...^.
......|........
והשאלה היא כמה עולמות אפשריים כאלה קיימים.
אז ברור שאנחנו לא יכולים "לנסות" את כל העולמות כלומר לחשב ולשמור כל מסלול אפשרי. זה פשוט ייקח הרבה יותר מדי פעולות בגלל החישובים הכפולים. וברגע שחשבתם את המילה "חישובים כפולים" מיד אתם יודעים שנכנסתם לארץ התכנות הדינמי.
תכנות דינמי בסך הכל אומר שאנחנו שומרים תוצאות של חישובי ביניים כדי לא לחזור עליהם. בסיפור של הקרן אני רוצה לסמן Slitter מסוים ולהסתכל עליו שימו לב לכוכבית בציור הבא:
.......S.......
.......|.......
......|^.......
......|........
......^|^......
.......|.......
.....^|^.^.....
......|........
....^.*|..^....
.......|.......
...^.^.|.^.^...
.......|.......
..^...^|....^..
.......|.......
.^.^.^|^.^...^.
......|........
הדרך הלא נכונה לחשוב על התרגיל הזה, וזו שתייצר חישובים כפולים, היא להגיד שאנחנו מגיעים לכוכבית ואז מפעילים קריאה רקורסיבית וסופרים כמה עולמות יש בצד שמאל שלה וכמה עולמות יש בצד ימין שלה. בשביל לראות למה זה לא עובד שימו לב לשני הספליטרים שמעל הכוכבית (מסומנים בכרוכית):
.......S.......
.......|.......
......|^.......
......|........
......^|^......
.......|.......
.....@|@.^.....
......|........
....^.*|..^....
.......|.......
...^.^.|.^.^...
.......|.......
..^...^|....^..
.......|.......
.^.^.^|^.^...^.
......|........
כל אחד מהם יכול לשלוח קרן שתגיע לכוכבית. החישוב הכפול קורה כשאנחנו סופרים את מספר העולמות מהכוכבית למטה פעמיים: פעם אחת כשהגענו לכוכבית מהכרוכית השמאלית ופעם שניה וזהה כשאנחנו מגיעים לכוכבית מהכרוכית הימנית. ספירת פתרונות לא עובדת בגלל שהיא מייצרת המון חישובים כפולים.
מה עושים במקום? תכנות דינמי אומר שכדאי לנו לשמור בכל משבצת כמה אפשרויות יש להגיע אליה. בכל שורה נוכל להסתכל על השורה הקודמת כדי לעדכן את המונה: אם מעלינו היה Splitter הערך החדש הוא 0, אחרת סוכמים את ערך המשבצת שמעלינו ואת האלכסונים שלמעלה אם הם החזיקו Splitter.
בסוף נוכל לסכום את כל האפשרויות של כל המשבצות בשורה האחרונה וכך נקבל את מספר העולמות.
הנה זה ברובי לא מסובך בכלל:
def part2
beams_through = []
File.read(@filename).lines.each do |line|
if beam_index = line =~ /S/
puts "--- START ---"
beams_through = line.chars.map {|c| c == "S" ? 1 : 0 }
end
splitter_indexes = Set.new(line.find_all_indexes('^'))
beams_through = beams_through.each_with_index.map do |previous_count, index|
if splitter_indexes.include?(index)
0
else
value = beams_through[index]
value += beams_through[index - 1] if splitter_indexes.include?(index - 1)
value += beams_through[index + 1] if splitter_indexes.include?(index + 1)
value
end
end
pp beams_through
end
beams_through.sum
end
פתרון Advent Of Code 2025 יום 8
הי חברים פוסט אחרון ברצף הזה של Advent Of Code כמו השניים הקודמים גם כאן יש לנו תרגיל שנראה פשוט ואז מגיע הטוויסט בחלק השני ואיתו הזדמנות לדבר על Trade Offs ופתרונות לא מושלמים. בואו נצא לדרך.
האתגר
יום 8 מציג לנו בתור קלט רשימה של מיקומים בעולם תלת מימדי:
אפשר לדמיין שאלה מיקומים של איזשהם רכיבים שמתחברים למעגלים חשמליים אבל זה לא מאוד חשוב. עכשיו מתחילים לולאה שמוצאת את שתי הנקודות הקרובות ביותר ומחברת אותן. איטרציה שניה מוצאת את שתי הנקודות הבאות הכי קרובות ומחברת אותן וכך ממשיכים n איטרציות. מגדירים קבוצה של נקודות מחוברות בתור "מעגל" והשאלה היא מה מכפלת הגדלים של שלושת המעגלים הגדולים ביותר.
הפתרון
נשים בצד את סיפור הרקע ונשים לב לדילמה - מה עולה ומתי משלמים. שתי אפשרויות:
1. אנחנו למצוא שתי נקודות קרובות ואז "לרשום" בצד שאנחנו מחברים אותן וכך לבנות מבנה נתונים של גרף. אחרי אלף חיבורים נסרוק את הגרף ונחפש מעגלים.
2. אנחנו יכולים אחרי כל חיבור להסתכל מה חיברנו ולאחד את הצמתים לגרף. במצב כזה הכתיבה היא יקרה כי כל פעם שאני יוצר חיבור בין p ל q אני צריך לסמן גם ש p מחובר ל q אבל גם שכל הנקודות שמחוברות ל p עכשיו מחוברות לכל הנקודות שמחוברות ל q כלומר לאחד את המעגלים.
מאחר ואנחנו יודעים מה מספר האיטרציות יהיה יותר קל לפתור את התרגיל בשיטה הראשונה. לצערי (או לשמחתי) זה לא מה שעשיתי והפתרון שכתבתי השתמש בשיטה השניה כלומר כל נקודה יוצגה על ידי קבוצה וכל חיבור איחד שתי קבוצות. זה אומר שכל איטרציה עולה קצת יותר אבל זיהוי המעגלים בסוף הוא בחינם. זה הקוד ברובי, הכי ארוך עד כה:
למרות שהפתרון נכון ועובד קלוד זיהה את ה"באג הקריטי" הבא:
הזיהוי הזה בדיוק מראה לנו למה עדיין קשה לקבל Code Review מ AI. כשקוראים את הקוד ואת ההערה זה נראה מאוד נכון, באמת אין סימטריה בפונקציה connect בין p ו q. לקבוצה של p אני רק עושה merge אבל בקבוצה של q אני ממש רץ בלולאה ומעדכן את כל ההפניות. למה לא הייתי צריך לעשות את זה גם ב p ?
התשובה ברורה למי שמבין את זרימת המידע במערכת - הנקודות האחרות ב p כבר מצביעות על אותו Set כמו p כי עדכנתי את ההצבעה שלהן בחיבורים קודמים. אין טעם לרוץ ולעדכן מחדש.
הבאג השני שקלוד מזהה בטעות ב Code Review הוא:
הי חברים פוסט אחרון ברצף הזה של Advent Of Code כמו השניים הקודמים גם כאן יש לנו תרגיל שנראה פשוט ואז מגיע הטוויסט בחלק השני ואיתו הזדמנות לדבר על Trade Offs ופתרונות לא מושלמים. בואו נצא לדרך.
האתגר
יום 8 מציג לנו בתור קלט רשימה של מיקומים בעולם תלת מימדי:
162,817,812
57,618,57
906,360,560
592,479,940
352,342,300
466,668,158
542,29,236
431,825,988
739,650,466
52,470,668
216,146,977
819,987,18
117,168,530
805,96,715
346,949,466
970,615,88
941,993,340
862,61,35
984,92,344
425,690,689
אפשר לדמיין שאלה מיקומים של איזשהם רכיבים שמתחברים למעגלים חשמליים אבל זה לא מאוד חשוב. עכשיו מתחילים לולאה שמוצאת את שתי הנקודות הקרובות ביותר ומחברת אותן. איטרציה שניה מוצאת את שתי הנקודות הבאות הכי קרובות ומחברת אותן וכך ממשיכים n איטרציות. מגדירים קבוצה של נקודות מחוברות בתור "מעגל" והשאלה היא מה מכפלת הגדלים של שלושת המעגלים הגדולים ביותר.
הפתרון
נשים בצד את סיפור הרקע ונשים לב לדילמה - מה עולה ומתי משלמים. שתי אפשרויות:
1. אנחנו למצוא שתי נקודות קרובות ואז "לרשום" בצד שאנחנו מחברים אותן וכך לבנות מבנה נתונים של גרף. אחרי אלף חיבורים נסרוק את הגרף ונחפש מעגלים.
2. אנחנו יכולים אחרי כל חיבור להסתכל מה חיברנו ולאחד את הצמתים לגרף. במצב כזה הכתיבה היא יקרה כי כל פעם שאני יוצר חיבור בין p ל q אני צריך לסמן גם ש p מחובר ל q אבל גם שכל הנקודות שמחוברות ל p עכשיו מחוברות לכל הנקודות שמחוברות ל q כלומר לאחד את המעגלים.
מאחר ואנחנו יודעים מה מספר האיטרציות יהיה יותר קל לפתור את התרגיל בשיטה הראשונה. לצערי (או לשמחתי) זה לא מה שעשיתי והפתרון שכתבתי השתמש בשיטה השניה כלומר כל נקודה יוצגה על ידי קבוצה וכל חיבור איחד שתי קבוצות. זה אומר שכל איטרציה עולה קצת יותר אבל זיהוי המעגלים בסוף הוא בחינם. זה הקוד ברובי, הכי ארוך עד כה:
def line_to_pos(l)
l.chomp.split(',').map(&:to_i)
end
def distance(p1, p2)
(p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2 + (p1[2] - p2[2]) ** 2
end
class Day8
def initialize(filename)
lines = File.read(filename).lines
@circuits = lines.map do |line|
pos = line_to_pos(line)
[pos, Set.new([pos])]
end.to_h
distances = Hash.new {|h, k| h[k] = [] }
lines.each do |from|
lines.each do |to|
next if from == to
p = line_to_pos(from)
q = line_to_pos(to)
d = distance(p, q)
distances[d] << [p, q] unless distances[d].include?([q, p])
end
end
@distances = distances.keys.sort.flat_map {|k| distances[k] }
end
def connect(p, q)
c_p = @circuits[p]
c_q = @circuits[q]
return 0 if c_p == c_q
@circuits[p].merge(c_q)
@circuits[q].each do |qq|
@circuits[qq] = @circuits[p]
end
return 1
end
def part1
cables = 1_000
i = 0
while cables > 0
next_p, next_q = @distances[i]
cables_used = connect(next_p, next_q)
cables -= 1
i += 1
end
@circuits.values.uniq.map {|s| s.size }.sort.reverse[0..2].reduce(1) {|acc, val| acc * val }
end
end
למרות שהפתרון נכון ועובד קלוד זיהה את ה"באג הקריטי" הבא:
Problem: Only elements in c_q get updated. Elements in c_p that aren't p may still point to the old set.
הזיהוי הזה בדיוק מראה לנו למה עדיין קשה לקבל Code Review מ AI. כשקוראים את הקוד ואת ההערה זה נראה מאוד נכון, באמת אין סימטריה בפונקציה connect בין p ו q. לקבוצה של p אני רק עושה merge אבל בקבוצה של q אני ממש רץ בלולאה ומעדכן את כל ההפניות. למה לא הייתי צריך לעשות את זה גם ב p ?
התשובה ברורה למי שמבין את זרימת המידע במערכת - הנקודות האחרות ב p כבר מצביעות על אותו Set כמו p כי עדכנתי את ההצבעה שלהן בחיבורים קודמים. אין טעם לרוץ ולעדכן מחדש.
הבאג השני שקלוד מזהה בטעות ב Code Review הוא:
Cables are decremented even when connect returns 0 (already connected):
לכאורה אם connect מחזיר 0 לא חיברנו כלום ולכן מספר החוטים לא ירד. זה נכון ותואם את שמות המשתנים אבל לא תואם את הגדרות השאלה. המשימה שלי היתה לבצע אלף איטרציות בלי קשר לכמות החיבורים שבוצעו או לא בוצעו. אני מבין למה קלוד חושב שמשהו פה מוזר וטוב שהוא מפנה את תשומת לבי לבחירת השמות הלא נכונה אבל נשים לב שאנחנו צריכים להיות מאוד זהירים בקריאת הערות Code Review שמגיעות מ AI כי בלי הבנה קשה לתת פידבק מועיל.
הטוויסט
החלק השני של השאלה הופך את הסיפור למאתגר. הפעם במקום לזהות מה הסיטואציה אחרי אלף איטרציות עלינו למצוא מי הנקודה שבחיבורה אנחנו מחברים את כל הנקודות למעגל אחד כלומר יש להמשיך בלולאת while עד שאין יותר מעגלים. עבור החלק הזה כבר אי אפשר לחכות לסוף עם ספירת המעגלים וחייבים להיות מסוגלים לזהות כמה מעגלים יש בזמן קבוע. זה הקוד ברובי שאחרי כל עבודת ההכנה שעשינו יצא די קצר:
ניסיון שני
למרות שהקוד עובד הוא עדיין דורש דקות ארוכות להחזיר תוצאה על קלט אמיתי, איחוד הקבוצות שביצעתי לא מספיק יעיל, במיוחד כשהקבוצות גדולות. חיפוש מעגלים אולי לוקח אפס זמן אבל שילמתי על זה יותר מדי בהכנסת פריטים.
התובנה החשובה כאן היא שבעצם אנחנו לא צריכים גישה מהירה לכל הקואורדינטות של כל הפריטים במעגל, אלא רק לדעת עבור כל נקודה לאיזה מעגל היא שייכת. שמירת המעגל בתור קבוצה וביצוע איחוד קבוצות עובד אבל לאט. מנגנון טוב יותר מתבסס על עץ:
1. כל נקודה מיוצגת על ידי צומת.
2. כל פעם שמחברים שתי נקודות אנחנו יוצרים צומת חדש שמחבר את שתיהן. בצורה כזאת כשנרצה לגלות לאיזה "מעגל" שייכת נקודה מסוימת נצטרך רק לרוץ במעלה העץ עד לשורש. כל שתי נקודות ששייכות לאותו מעגל גם יהיו שייכות לאותו עץ.
3. בשביל לדעת אם חיברנו את כל הנקודות נוכל להשתמש במונה: כל פעם שאנחנו יוצרים חיבור חדש נוסיף 1 וברגע שנגיע למספר הנקודות פחות 1 זה אומר שכל הנקודות מחוברות אחת לשניה.
זה הקוד של המחלקה Circuit שמנהלת את העץ:
וקוד הפתרון בעזרת Circuit הוא עכשיו:
הפעם מקבלים את אותה תשובה נכונה הרבה יותר מהר.
הטוויסט
החלק השני של השאלה הופך את הסיפור למאתגר. הפעם במקום לזהות מה הסיטואציה אחרי אלף איטרציות עלינו למצוא מי הנקודה שבחיבורה אנחנו מחברים את כל הנקודות למעגל אחד כלומר יש להמשיך בלולאת while עד שאין יותר מעגלים. עבור החלק הזה כבר אי אפשר לחכות לסוף עם ספירת המעגלים וחייבים להיות מסוגלים לזהות כמה מעגלים יש בזמן קבוע. זה הקוד ברובי שאחרי כל עבודת ההכנה שעשינו יצא די קצר:
def part2
i = 0
while @circuits.values.uniq.size > 1
next_p, next_q = @distances[i]
connect(next_p, next_q)
i += 1
pp "#{@circuits.values.uniq.size} circuits remaining"
end
next_p[0] * next_q[0]
end
ניסיון שני
למרות שהקוד עובד הוא עדיין דורש דקות ארוכות להחזיר תוצאה על קלט אמיתי, איחוד הקבוצות שביצעתי לא מספיק יעיל, במיוחד כשהקבוצות גדולות. חיפוש מעגלים אולי לוקח אפס זמן אבל שילמתי על זה יותר מדי בהכנסת פריטים.
התובנה החשובה כאן היא שבעצם אנחנו לא צריכים גישה מהירה לכל הקואורדינטות של כל הפריטים במעגל, אלא רק לדעת עבור כל נקודה לאיזה מעגל היא שייכת. שמירת המעגל בתור קבוצה וביצוע איחוד קבוצות עובד אבל לאט. מנגנון טוב יותר מתבסס על עץ:
1. כל נקודה מיוצגת על ידי צומת.
2. כל פעם שמחברים שתי נקודות אנחנו יוצרים צומת חדש שמחבר את שתיהן. בצורה כזאת כשנרצה לגלות לאיזה "מעגל" שייכת נקודה מסוימת נצטרך רק לרוץ במעלה העץ עד לשורש. כל שתי נקודות ששייכות לאותו מעגל גם יהיו שייכות לאותו עץ.
3. בשביל לדעת אם חיברנו את כל הנקודות נוכל להשתמש במונה: כל פעם שאנחנו יוצרים חיבור חדש נוסיף 1 וברגע שנגיע למספר הנקודות פחות 1 זה אומר שכל הנקודות מחוברות אחת לשניה.
זה הקוד של המחלקה Circuit שמנהלת את העץ:
class Circuit
attr_accessor :parent, :name
def initialize(name)
self.name = name
self.parent = self
end
def merge(other)
self.parent = self.parent.parent while self.parent.parent != self.parent
other.parent = other.parent.parent while other.parent.parent != other.parent
return 0 if self.parent == other.parent
c = Circuit.new("#{self.name}<->#{other.name}")
self.parent.parent = c
other.parent.parent = c
return 1
end
end
וקוד הפתרון בעזרת Circuit הוא עכשיו:
def part2
@circuits = @circuits.map {|k, v| [k, Circuit.new(k.to_s)] }.to_h
cables = @circuits.size - 1
while cables > 0
next_p, next_q = @distances.shift
cables -= @circuits[next_p].merge(@circuits[next_q])
pp "connected #{next_p[0]}, #{next_q[0]}. #{cables} cables remaining"
end
next_p[0] * next_q[0]
end
הפעם מקבלים את אותה תשובה נכונה הרבה יותר מהר.
משתף את הפוסט הזה של אדי אוסמני 21 שעורים על הנדסת תוכנה כל מילה זהב
https://addyosmani.com/blog/21-lessons/
https://addyosmani.com/blog/21-lessons/
Addyosmani
21 Lessons From 14 Years at Google
Lessons learned from 14 years of engineering at Google, focusing on what truly matters beyond just writing great code.
❤1
אם אני אפילו לא מבין את השורה הראשונה
קיבלת קוד שעובד, הצלחת לקמפל ולהריץ ובתור משתמש אתה לא מצליח לזהות אף בעיה, אבל יש רק בעיה אחת בתור מפתח: אתה לא מצליח להבין אפילו שורה אחת מכל הערימה.
רוצים דוגמה? מספיק לבקש מ AI שיכתוב לכם מערכת Full Stack בטכנולוגיה שאתם לא מכירים. ושתעשה משהו מסובך. בהנחה האופטימית שאחרי כמה איטרציות המערכת עובדת חזרנו לשאלת הפתיחה - מה עושים עם זה? מה שווה קוד אם אנחנו לא יכולים להבין אותו?
הדילמה פה ברורה ויושבת בגדול בין שלוש אפשרויות:
1. אפשרות ראשונה - אני מכניס למערכת רק דברים שאני יכול להבין ושהייתי יכול לכתוב לבד. אם אתם כאלה ה AI חסך לכם חלק מזמן הלימוד בכך שהחומר יותר זמין ואפשר לשאול אבל הוא לא חסך את זמן ההבנה. ניקח כדוגמה את ריילס אז אם אף פעם לא כתבתם כלום בריילס ואתם מקבלים מערכת מלאה בריילס מה AI אתם תצטרכו כמה שבועות כדי לפענח מה קורה שם. אלה שבועות מאוד מתסכלים כי המערכת לכאורה עובדת. אם תמיד חשבנו שאנחנו מבינים בשביל לפתח, על מה אנחנו מבזבזים זמן עכשיו?
2. אפשרות שניה - אני מכניס למערכת מה שעובד, אין לי מה לבזבז זמן על קריאת קוד. אם זה עובד אני מרוצה כמו שאת הקוד של ספריות בסיס ופריימוורקים אני לא קורא ומבין אם ה AI כתב וזה עובד הוא יודע מה הוא עושה. אם תהיה בעיה נשבור על זה את הראש בהמשך.
3. אפשרות שלישית, או כמו שה AI אוהב להגיד הפתרון ההיברידי, אני עובר על הקוד בגדול, מנסה להבין מה שאני מצליח ומתישהו גם בלי להבין הכל משלב את הקוד בפרויקט ומתקדם.
שלושת האפשרויות גרועות.
הראשונה גרועה כי היא מרגישה כמו בזבוז זמן, יש אנשים מחכים, כולם רוצים להתקדם וארגונית אנחנו לא בנויים למשיכות זמן כאלה. השנייה גרועה כי טעויות שעושים היום עלולות להתגלות רק בעתיד והתשלום עליהן יהיה בריבית. השלישית פשוט לוקחת את הרע משתי האפשרויות האחרות, אנחנו גם מבזבזים זמן וגם בסוף מכניסים טעויות.
הדרך לצאת מהדילמה היא להבין איך להפוך לבן אדם שכן מבין את כל מה שה AI כותב. לא כי עברת שורה שורה וזה נראה הגיוני אלא כי אתה ממילא מכיר את עולם התוכן טוב יותר מה AI. כשה AI כותב משהו שלא נראה הגיוני אתה יודע שאתה צודק ולא הוא. אתה מבין מאיפה הוא הביא את ההצעה ויודע למה היא לא נכונה (או היתה נכונה והיום כבר לא, או נכונה בנסיבות אחרות).
וכן זאת השקעה שדורשת זמן. וכן אני יודע שכולם צריכים תשובה עכשיו והקוד מחכה והמשקיע מחכה והבוס מחכה והלוואי והיה פתרון קל. החכם אומר שהזמן הכי טוב לנטוע עץ היה לפני עשרים שנה. עכשיו זה רק הזמן השני הכי טוב.
קיבלת קוד שעובד, הצלחת לקמפל ולהריץ ובתור משתמש אתה לא מצליח לזהות אף בעיה, אבל יש רק בעיה אחת בתור מפתח: אתה לא מצליח להבין אפילו שורה אחת מכל הערימה.
רוצים דוגמה? מספיק לבקש מ AI שיכתוב לכם מערכת Full Stack בטכנולוגיה שאתם לא מכירים. ושתעשה משהו מסובך. בהנחה האופטימית שאחרי כמה איטרציות המערכת עובדת חזרנו לשאלת הפתיחה - מה עושים עם זה? מה שווה קוד אם אנחנו לא יכולים להבין אותו?
הדילמה פה ברורה ויושבת בגדול בין שלוש אפשרויות:
1. אפשרות ראשונה - אני מכניס למערכת רק דברים שאני יכול להבין ושהייתי יכול לכתוב לבד. אם אתם כאלה ה AI חסך לכם חלק מזמן הלימוד בכך שהחומר יותר זמין ואפשר לשאול אבל הוא לא חסך את זמן ההבנה. ניקח כדוגמה את ריילס אז אם אף פעם לא כתבתם כלום בריילס ואתם מקבלים מערכת מלאה בריילס מה AI אתם תצטרכו כמה שבועות כדי לפענח מה קורה שם. אלה שבועות מאוד מתסכלים כי המערכת לכאורה עובדת. אם תמיד חשבנו שאנחנו מבינים בשביל לפתח, על מה אנחנו מבזבזים זמן עכשיו?
2. אפשרות שניה - אני מכניס למערכת מה שעובד, אין לי מה לבזבז זמן על קריאת קוד. אם זה עובד אני מרוצה כמו שאת הקוד של ספריות בסיס ופריימוורקים אני לא קורא ומבין אם ה AI כתב וזה עובד הוא יודע מה הוא עושה. אם תהיה בעיה נשבור על זה את הראש בהמשך.
3. אפשרות שלישית, או כמו שה AI אוהב להגיד הפתרון ההיברידי, אני עובר על הקוד בגדול, מנסה להבין מה שאני מצליח ומתישהו גם בלי להבין הכל משלב את הקוד בפרויקט ומתקדם.
שלושת האפשרויות גרועות.
הראשונה גרועה כי היא מרגישה כמו בזבוז זמן, יש אנשים מחכים, כולם רוצים להתקדם וארגונית אנחנו לא בנויים למשיכות זמן כאלה. השנייה גרועה כי טעויות שעושים היום עלולות להתגלות רק בעתיד והתשלום עליהן יהיה בריבית. השלישית פשוט לוקחת את הרע משתי האפשרויות האחרות, אנחנו גם מבזבזים זמן וגם בסוף מכניסים טעויות.
הדרך לצאת מהדילמה היא להבין איך להפוך לבן אדם שכן מבין את כל מה שה AI כותב. לא כי עברת שורה שורה וזה נראה הגיוני אלא כי אתה ממילא מכיר את עולם התוכן טוב יותר מה AI. כשה AI כותב משהו שלא נראה הגיוני אתה יודע שאתה צודק ולא הוא. אתה מבין מאיפה הוא הביא את ההצעה ויודע למה היא לא נכונה (או היתה נכונה והיום כבר לא, או נכונה בנסיבות אחרות).
וכן זאת השקעה שדורשת זמן. וכן אני יודע שכולם צריכים תשובה עכשיו והקוד מחכה והמשקיע מחכה והבוס מחכה והלוואי והיה פתרון קל. החכם אומר שהזמן הכי טוב לנטוע עץ היה לפני עשרים שנה. עכשיו זה רק הזמן השני הכי טוב.
❤1
חדשנות, ספריות, AI
ספריות חיצוניות במידה רבה וקוד תשתית פנימי במידה יותר קטנה קובעים את הגבולות של האפשרי. הם החוזה שלנו עם העולם. וכן אפשר להפר חוזה אבל לפעמים יש קנס. ניקח ספריית רובי בשם
https://github.com/mbleigh/acts-as-taggable-on
הספריה מצוינת ופופולרית אבל לא כוללת מנגנון לעדכון כמות גדולה של ישויות עם התיוגים שלהם, כלומר אפשר לכתוב:
אבל אם נכתוב:
נבצע המון פעולות insert וזה כנראה יעלה בביצועים.
כמה מחשבות סביב זה בעקבות התנגשויות בין פיצ'רים:
1. סוכני קידוד אוהבים את הסטנדרט. שימוש בספריה בגרסה המקורית שלה עושה המון שכל ומאפשר להם לעבוד מהר.
2. ספריה היא חוזה. שימוש בספריה בגרסה המקורית שלה מאפשרת לי ליהנות משדרוגים ותחזוקה, כל עוד אני משחק לפי הכללים.
3. קוד הספריה עצמו מתבסס על ההכנסה שורה-שורה. הרבה דברים בתוך הקוד כנראה יישברו אם אני אנסה להכניס ב batch אפילו שאני יודע איך הטבלה שלהם בנויה.
4. אני יכול לבנות מחדש את כל המנגנונים שיישברו בעת מימוש הכנסה ב batch, אבל אף אחד לא מבטיח לי שבעדכון הספריה הבא התיקונים שלי ימשיכו לעבוד.
ב 2026 יש איזון עדין בין המחויבות והחוזה שמגיעים עם ספריה חיצונית לבין מימוש לבד של פתרון קטן רק בשביל להתקדם. אם הקוד צריך לטפל בהמון מקרים מורכבים ומקרי קצה שאני אפילו לא מדמיין עדיף לחפש ספריה. אם אני עדיין לא בטוח מה דרישות המוצר שלי עדיף לתת ל AI לבנות משהו מהר ולהשתמש ב API של ספריה קיימת בתור בסיס. מקסימום בהמשך נוכל להחליף.
ספריות חיצוניות במידה רבה וקוד תשתית פנימי במידה יותר קטנה קובעים את הגבולות של האפשרי. הם החוזה שלנו עם העולם. וכן אפשר להפר חוזה אבל לפעמים יש קנס. ניקח ספריית רובי בשם
acts-as-taggable-on בשביל הדוגמה:https://github.com/mbleigh/acts-as-taggable-on
הספריה מצוינת ופופולרית אבל לא כוללת מנגנון לעדכון כמות גדולה של ישויות עם התיוגים שלהם, כלומר אפשר לכתוב:
@another_user.skill_list.add("clowning")
אבל אם נכתוב:
all_users.each do |u|
u.skill_list.add("clowning")
u.save
end
נבצע המון פעולות insert וזה כנראה יעלה בביצועים.
כמה מחשבות סביב זה בעקבות התנגשויות בין פיצ'רים:
1. סוכני קידוד אוהבים את הסטנדרט. שימוש בספריה בגרסה המקורית שלה עושה המון שכל ומאפשר להם לעבוד מהר.
2. ספריה היא חוזה. שימוש בספריה בגרסה המקורית שלה מאפשרת לי ליהנות משדרוגים ותחזוקה, כל עוד אני משחק לפי הכללים.
3. קוד הספריה עצמו מתבסס על ההכנסה שורה-שורה. הרבה דברים בתוך הקוד כנראה יישברו אם אני אנסה להכניס ב batch אפילו שאני יודע איך הטבלה שלהם בנויה.
4. אני יכול לבנות מחדש את כל המנגנונים שיישברו בעת מימוש הכנסה ב batch, אבל אף אחד לא מבטיח לי שבעדכון הספריה הבא התיקונים שלי ימשיכו לעבוד.
ב 2026 יש איזון עדין בין המחויבות והחוזה שמגיעים עם ספריה חיצונית לבין מימוש לבד של פתרון קטן רק בשביל להתקדם. אם הקוד צריך לטפל בהמון מקרים מורכבים ומקרי קצה שאני אפילו לא מדמיין עדיף לחפש ספריה. אם אני עדיין לא בטוח מה דרישות המוצר שלי עדיף לתת ל AI לבנות משהו מהר ולהשתמש ב API של ספריה קיימת בתור בסיס. מקסימום בהמשך נוכל להחליף.
GitHub
GitHub - mbleigh/acts-as-taggable-on: A tagging plugin for Rails applications that allows for custom tagging along dynamic contexts.
A tagging plugin for Rails applications that allows for custom tagging along dynamic contexts. - mbleigh/acts-as-taggable-on
❤1
טיפ פייתון: תבניות של מחרוזות
שאלה שעלתה אתמול בטלגרם הסבה את תשומת לבי למנגנון לא חדש בפייתון אבל כזה שיכול לפתור בעיה ספציפית בצורה מדויקת ואני מדבר על str.format. בואו נראה את זה.
האתגר
נכתוב פונקציה שיוצרת קבצים בלולאה, היא מקבלת מספר קבצים ליצור ותחילית ויוצרת את הקבצים לפי הסדר:
עכשיו נשפר את הפונקציה כדי לאפשר למי שקורא לה להעביר תבנית מלאה לשם קובץ. לא רק תחילית אלא גם סיומת כדי שהקבצים יהיו למשל demo1.txt, demo2.txt.
פתרון 1 - העברת פונקציה
בפוסט קודם בנושא הצעתי להעביר פונקציה עבור prefix וזה נראה כך:
זה עובד אבל קצת מסורבל ומכריח את כל מי שמפעיל את הפונקציה לדעת מה זה lambda.
פתרון 2 - תבנית למחרוזת
פייתון כוללת כבר המון זמן מנגנון של פענוח מושהה של משתנים בתוך מחרוזת בעזרת פקודת format. זה נראה כך:
הרבה יותר קל גם למי שמפעיל את הפונקציה וגם למי שכותב אותה.
נ.ב. בטלגרם עניתי שאפשר אולי להשתמש ב t-strings כדי לכתוב את התבנית אבל האמת שזאת היתה טעות. format קל יותר, נכון יותר למצב הזה ועובד. מנגנון ה t-string החדש של פייתון 3.14 מבצע את השערוך בזמן יצירת המחרוזת בדיוק כמו f-string ולכן לא היה עובד כאן.
שאלה שעלתה אתמול בטלגרם הסבה את תשומת לבי למנגנון לא חדש בפייתון אבל כזה שיכול לפתור בעיה ספציפית בצורה מדויקת ואני מדבר על str.format. בואו נראה את זה.
האתגר
נכתוב פונקציה שיוצרת קבצים בלולאה, היא מקבלת מספר קבצים ליצור ותחילית ויוצרת את הקבצים לפי הסדר:
def create_files(n, prefix):
for i in range(n):
with open(f"{prefix}{i}", "a") as f:
pass
עכשיו נשפר את הפונקציה כדי לאפשר למי שקורא לה להעביר תבנית מלאה לשם קובץ. לא רק תחילית אלא גם סיומת כדי שהקבצים יהיו למשל demo1.txt, demo2.txt.
פתרון 1 - העברת פונקציה
בפוסט קודם בנושא הצעתי להעביר פונקציה עבור prefix וזה נראה כך:
def create_files(n, filename_template):
for i in range(n):
filename = filename_template(i)
with open(filename, "a") as f:
pass
create_files(5, lambda i: f"demo{i}.txt")
זה עובד אבל קצת מסורבל ומכריח את כל מי שמפעיל את הפונקציה לדעת מה זה lambda.
פתרון 2 - תבנית למחרוזת
פייתון כוללת כבר המון זמן מנגנון של פענוח מושהה של משתנים בתוך מחרוזת בעזרת פקודת format. זה נראה כך:
def create_files(n, filename_template):
for i in range(n):
filename = filename_template.format(i=i)
with open(filename, "a") as f:
pass
create_files(5, "demo{i}.txt")
הרבה יותר קל גם למי שמפעיל את הפונקציה וגם למי שכותב אותה.
נ.ב. בטלגרם עניתי שאפשר אולי להשתמש ב t-strings כדי לכתוב את התבנית אבל האמת שזאת היתה טעות. format קל יותר, נכון יותר למצב הזה ועובד. מנגנון ה t-string החדש של פייתון 3.14 מבצע את השערוך בזמן יצירת המחרוזת בדיוק כמו f-string ולכן לא היה עובד כאן.
👍2
טיילווינד. AI. שינויים.
טיילווינד הפך בשנים האחרונות מרעיון הזוי לדרך הסטנדרטית בה אנשים כותבים CSS. אם אתם מפתחי ווב בוודאות נתקלתם בו וכנראה גם כתבתם בו. כשאדם וותן התחיל את טיילווינד הוא בנה ספריית תבניות שמדגימה איך כדאי להשתמש בטיילווינד כדי לבנות דברים יפים. הספריה נקראה tailwind ui היום היא נקראת tailwind plus והמכירות שלה עוזרות לאדם להחזיק את העסק ולהמשיך לפתח את הפריימוורק.
ואז הגיע AI.
סוכני קידוד אולי לא מושלמים ולא מבינים את העומקים של המערכת אבל לכתוב CSS הם יודעים ולכתוב טיילווינד הם יודעים ממש טוב. כשקניתי את tailwind ui הרגשתי שעשיתי את הקניה של החיים. ברור זה לא היה זול אבל זה חסך לי שעות של כתיבת CSS ונתן לי קומפוננטות שנראו טוב לשלב בכל פרויקט. נהניתי מהתבניות, נהניתי לגזור חלקים ולהדביק בקוד שלי, אהבתי שאני יכול בשינויי קוד קטנים לשנות את איך שדברים נראים. בקיצור הייתי לקוח מרוצה. ואז הגיע AI והפסקתי לכתוב HTML-ים, CSS-ים ואת כל הקלאסים של טיילווינד. בתחילת הפרויקט אני נותן ל AI גם לעצב וגם לכתוב את ה CSS וכשפרויקט מתחיל להבשיל אני מחבר את ה MCP של פיגמה ונותן ל AI לדבר ישר עם המעצבים. בעיה פתורה. ואני לא היחיד.
אדם עדכן לאחרונה שהוא פיטר את רוב העובדים ב tailwind labs. אני מדביק כאן את ההודעה שלו כי היא מספרת המון על AI ועל השינויים שהוא הביא ומביא לתעשייה:
> I totally see the value in the feature and I would like to find a way to add it.
> But the reality is that 75% of the people on our engineering team lost their jobs here yesterday because of the brutal impact AI has had on our business. And every second I spend trying to do fun free things for the community like this is a second I'm not spending trying to turn the business around and make sure the people who are still here are getting their paychecks every month.
> Traffic to our docs is down about 40% from early 2023 despite Tailwind being more popular than ever. The docs are the only way people find out about our commercial products, and without customers we can't afford to maintain the framework. I really want to figure out a way to offer LLM-optimized docs that don't make that situation even worse (again we literally had to lay off 75% of the team yesterday), but I can't prioritize it right now unfortunately, and I'm nervous to offer them without solving that problem first.
> @PaulRBerg I don't see the AGENTS.md stuff we offer as part of the sponsorship program as anything similar to this at all — that's just a short markdown file with a bunch of my own personal opinions and what I consider best practices to nudge LLMs into writing their Tailwind stuff in a specific way. It's not the docs at all, and I resent the accusation that I am not disclosing my "true intentions" here or something.
> @mtsears4 Tailwind is growing faster than it ever has and is bigger than it ever has been, and our revenue is down close to 80%. Right now there's just no correlation between making Tailwind easier to use and making development of the framework more sustainable. I need to fix that before making Tailwind easier to use benefits anyone, because if I can't fix that this project is going to become unmaintained abandonware when there is no one left employed to work on it. I appreciate the sentiment and agree in spirit, it's just more complicated than that in reality right now.
נשים לב ירידה של 40% בתנועה לאתר, ירידה של 80% ברווח. זה ברור כי אף אחד כבר לא צריך לחפש בתיעוד שלהם איך קוראים לקלאס מסוים. ה AI כבר יודע. הלקוחות שלהם היו אותם מפתחים שבנו מערכת בלי מעצב ורצו משהו שיראה טוב וגם הכירו את העשייה של אדם ואת המהפיכה ש tailwind הביא. האנשים האלה התמעטו מאוד.
טיילווינד הפך בשנים האחרונות מרעיון הזוי לדרך הסטנדרטית בה אנשים כותבים CSS. אם אתם מפתחי ווב בוודאות נתקלתם בו וכנראה גם כתבתם בו. כשאדם וותן התחיל את טיילווינד הוא בנה ספריית תבניות שמדגימה איך כדאי להשתמש בטיילווינד כדי לבנות דברים יפים. הספריה נקראה tailwind ui היום היא נקראת tailwind plus והמכירות שלה עוזרות לאדם להחזיק את העסק ולהמשיך לפתח את הפריימוורק.
ואז הגיע AI.
סוכני קידוד אולי לא מושלמים ולא מבינים את העומקים של המערכת אבל לכתוב CSS הם יודעים ולכתוב טיילווינד הם יודעים ממש טוב. כשקניתי את tailwind ui הרגשתי שעשיתי את הקניה של החיים. ברור זה לא היה זול אבל זה חסך לי שעות של כתיבת CSS ונתן לי קומפוננטות שנראו טוב לשלב בכל פרויקט. נהניתי מהתבניות, נהניתי לגזור חלקים ולהדביק בקוד שלי, אהבתי שאני יכול בשינויי קוד קטנים לשנות את איך שדברים נראים. בקיצור הייתי לקוח מרוצה. ואז הגיע AI והפסקתי לכתוב HTML-ים, CSS-ים ואת כל הקלאסים של טיילווינד. בתחילת הפרויקט אני נותן ל AI גם לעצב וגם לכתוב את ה CSS וכשפרויקט מתחיל להבשיל אני מחבר את ה MCP של פיגמה ונותן ל AI לדבר ישר עם המעצבים. בעיה פתורה. ואני לא היחיד.
אדם עדכן לאחרונה שהוא פיטר את רוב העובדים ב tailwind labs. אני מדביק כאן את ההודעה שלו כי היא מספרת המון על AI ועל השינויים שהוא הביא ומביא לתעשייה:
> I totally see the value in the feature and I would like to find a way to add it.
> But the reality is that 75% of the people on our engineering team lost their jobs here yesterday because of the brutal impact AI has had on our business. And every second I spend trying to do fun free things for the community like this is a second I'm not spending trying to turn the business around and make sure the people who are still here are getting their paychecks every month.
> Traffic to our docs is down about 40% from early 2023 despite Tailwind being more popular than ever. The docs are the only way people find out about our commercial products, and without customers we can't afford to maintain the framework. I really want to figure out a way to offer LLM-optimized docs that don't make that situation even worse (again we literally had to lay off 75% of the team yesterday), but I can't prioritize it right now unfortunately, and I'm nervous to offer them without solving that problem first.
> @PaulRBerg I don't see the AGENTS.md stuff we offer as part of the sponsorship program as anything similar to this at all — that's just a short markdown file with a bunch of my own personal opinions and what I consider best practices to nudge LLMs into writing their Tailwind stuff in a specific way. It's not the docs at all, and I resent the accusation that I am not disclosing my "true intentions" here or something.
> @mtsears4 Tailwind is growing faster than it ever has and is bigger than it ever has been, and our revenue is down close to 80%. Right now there's just no correlation between making Tailwind easier to use and making development of the framework more sustainable. I need to fix that before making Tailwind easier to use benefits anyone, because if I can't fix that this project is going to become unmaintained abandonware when there is no one left employed to work on it. I appreciate the sentiment and agree in spirit, it's just more complicated than that in reality right now.
נשים לב ירידה של 40% בתנועה לאתר, ירידה של 80% ברווח. זה ברור כי אף אחד כבר לא צריך לחפש בתיעוד שלהם איך קוראים לקלאס מסוים. ה AI כבר יודע. הלקוחות שלהם היו אותם מפתחים שבנו מערכת בלי מעצב ורצו משהו שיראה טוב וגם הכירו את העשייה של אדם ואת המהפיכה ש tailwind הביא. האנשים האלה התמעטו מאוד.
בדיקות הן שפה
החלק הכי חשוב בכתיבת בדיקות הוא לא כתיבת הבדיקות עצמן אלא בניית התשתית איתה אנחנו כותבים את הבדיקות. נשווה בין שתי אפשרויות לכתיבת בדיקות אוטומטיות לאתר:
1. אפשרות ראשונה הבדיקה תגיד "כנס לדף הבית, לחץ על תיבת הטקסט באמצע, תכתוב מילת חיפוש, לחץ אישור, בעמוד שהתקבל תחפש רשימת פריטים ותראה שלפריט הראשון יש את הטקסט X.
2. אפשרות שניה הבדיקה תגיד "צור ויקי עם שלושה דפים X, Y ו Z. חפש X. תראה שהתוצאה הראשונה היא X."
מעבר ליותר שליטה היתרון הכי גדול של הגישה השניה הוא קביעת המילים. הגדרת היכולות של המערכת. המערכת שלי יודעת לבנות ויקי. היא יודעת לחפש והיא יודעת להציג תוצאות. הגישה השניה מטעינה את הבדיקה במשמעות, והיא דורשת חשיבה, הבנה וכתיבת תשתית (בדרך כלל בתבנית שנקראת Page Object Model9).
בדיקות הן שפה. ושפה טובה הכרחית כדי לתקשר משמעות. בעולם הפיתוח משולב AI בו אנחנו חיים, שפה טובה פותחת ל AI את האפשרות להיות הרבה יותר פרודוקטיבי.
החלק הכי חשוב בכתיבת בדיקות הוא לא כתיבת הבדיקות עצמן אלא בניית התשתית איתה אנחנו כותבים את הבדיקות. נשווה בין שתי אפשרויות לכתיבת בדיקות אוטומטיות לאתר:
1. אפשרות ראשונה הבדיקה תגיד "כנס לדף הבית, לחץ על תיבת הטקסט באמצע, תכתוב מילת חיפוש, לחץ אישור, בעמוד שהתקבל תחפש רשימת פריטים ותראה שלפריט הראשון יש את הטקסט X.
2. אפשרות שניה הבדיקה תגיד "צור ויקי עם שלושה דפים X, Y ו Z. חפש X. תראה שהתוצאה הראשונה היא X."
מעבר ליותר שליטה היתרון הכי גדול של הגישה השניה הוא קביעת המילים. הגדרת היכולות של המערכת. המערכת שלי יודעת לבנות ויקי. היא יודעת לחפש והיא יודעת להציג תוצאות. הגישה השניה מטעינה את הבדיקה במשמעות, והיא דורשת חשיבה, הבנה וכתיבת תשתית (בדרך כלל בתבנית שנקראת Page Object Model9).
בדיקות הן שפה. ושפה טובה הכרחית כדי לתקשר משמעות. בעולם הפיתוח משולב AI בו אנחנו חיים, שפה טובה פותחת ל AI את האפשרות להיות הרבה יותר פרודוקטיבי.