ToCode
1.42K subscribers
3.83K links
טיפים קצרים למתכנתים מאת ינון פרק
Download Telegram
פתרון Advent Of Code 2025 יום 7
אני רוצה להמשיך בסדרת הפתרונות של 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 מציג לנו בתור קלט רשימה של מיקומים בעולם תלת מימדי:

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 עד שאין יותר מעגלים. עבור החלק הזה כבר אי אפשר לחכות לסוף עם ספירת המעגלים וחייבים להיות מסוגלים לזהות כמה מעגלים יש בזמן קבוע. זה הקוד ברובי שאחרי כל עבודת ההכנה שעשינו יצא די קצר:

  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


הפעם מקבלים את אותה תשובה נכונה הרבה יותר מהר.
אם אני אפילו לא מבין את השורה הראשונה
קיבלת קוד שעובד, הצלחת לקמפל ולהריץ ובתור משתמש אתה לא מצליח לזהות אף בעיה, אבל יש רק בעיה אחת בתור מפתח: אתה לא מצליח להבין אפילו שורה אחת מכל הערימה.

רוצים דוגמה? מספיק לבקש מ AI שיכתוב לכם מערכת Full Stack בטכנולוגיה שאתם לא מכירים. ושתעשה משהו מסובך. בהנחה האופטימית שאחרי כמה איטרציות המערכת עובדת חזרנו לשאלת הפתיחה - מה עושים עם זה? מה שווה קוד אם אנחנו לא יכולים להבין אותו?

הדילמה פה ברורה ויושבת בגדול בין שלוש אפשרויות:

1. אפשרות ראשונה - אני מכניס למערכת רק דברים שאני יכול להבין ושהייתי יכול לכתוב לבד. אם אתם כאלה ה AI חסך לכם חלק מזמן הלימוד בכך שהחומר יותר זמין ואפשר לשאול אבל הוא לא חסך את זמן ההבנה. ניקח כדוגמה את ריילס אז אם אף פעם לא כתבתם כלום בריילס ואתם מקבלים מערכת מלאה בריילס מה AI אתם תצטרכו כמה שבועות כדי לפענח מה קורה שם. אלה שבועות מאוד מתסכלים כי המערכת לכאורה עובדת. אם תמיד חשבנו שאנחנו מבינים בשביל לפתח, על מה אנחנו מבזבזים זמן עכשיו?

2. אפשרות שניה - אני מכניס למערכת מה שעובד, אין לי מה לבזבז זמן על קריאת קוד. אם זה עובד אני מרוצה כמו שאת הקוד של ספריות בסיס ופריימוורקים אני לא קורא ומבין אם ה AI כתב וזה עובד הוא יודע מה הוא עושה. אם תהיה בעיה נשבור על זה את הראש בהמשך.

3. אפשרות שלישית, או כמו שה AI אוהב להגיד הפתרון ההיברידי, אני עובר על הקוד בגדול, מנסה להבין מה שאני מצליח ומתישהו גם בלי להבין הכל משלב את הקוד בפרויקט ומתקדם.

שלושת האפשרויות גרועות.

הראשונה גרועה כי היא מרגישה כמו בזבוז זמן, יש אנשים מחכים, כולם רוצים להתקדם וארגונית אנחנו לא בנויים למשיכות זמן כאלה. השנייה גרועה כי טעויות שעושים היום עלולות להתגלות רק בעתיד והתשלום עליהן יהיה בריבית. השלישית פשוט לוקחת את הרע משתי האפשרויות האחרות, אנחנו גם מבזבזים זמן וגם בסוף מכניסים טעויות.

הדרך לצאת מהדילמה היא להבין איך להפוך לבן אדם שכן מבין את כל מה שה AI כותב. לא כי עברת שורה שורה וזה נראה הגיוני אלא כי אתה ממילא מכיר את עולם התוכן טוב יותר מה AI. כשה AI כותב משהו שלא נראה הגיוני אתה יודע שאתה צודק ולא הוא. אתה מבין מאיפה הוא הביא את ההצעה ויודע למה היא לא נכונה (או היתה נכונה והיום כבר לא, או נכונה בנסיבות אחרות).

וכן זאת השקעה שדורשת זמן. וכן אני יודע שכולם צריכים תשובה עכשיו והקוד מחכה והמשקיע מחכה והבוס מחכה והלוואי והיה פתרון קל. החכם אומר שהזמן הכי טוב לנטוע עץ היה לפני עשרים שנה. עכשיו זה רק הזמן השני הכי טוב.
1
חדשנות, ספריות, AI
ספריות חיצוניות במידה רבה וקוד תשתית פנימי במידה יותר קטנה קובעים את הגבולות של האפשרי. הם החוזה שלנו עם העולם. וכן אפשר להפר חוזה אבל לפעמים יש קנס. ניקח ספריית רובי בשם 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 של ספריה קיימת בתור בסיס. מקסימום בהמשך נוכל להחליף.
1
טיפ פייתון: תבניות של מחרוזות
שאלה שעלתה אתמול בטלגרם הסבה את תשומת לבי למנגנון לא חדש בפייתון אבל כזה שיכול לפתור בעיה ספציפית בצורה מדויקת ואני מדבר על 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 הביא. האנשים האלה התמעטו מאוד.
בדיקות הן שפה
החלק הכי חשוב בכתיבת בדיקות הוא לא כתיבת הבדיקות עצמן אלא בניית התשתית איתה אנחנו כותבים את הבדיקות. נשווה בין שתי אפשרויות לכתיבת בדיקות אוטומטיות לאתר:

1. אפשרות ראשונה הבדיקה תגיד "כנס לדף הבית, לחץ על תיבת הטקסט באמצע, תכתוב מילת חיפוש, לחץ אישור, בעמוד שהתקבל תחפש רשימת פריטים ותראה שלפריט הראשון יש את הטקסט X.

2. אפשרות שניה הבדיקה תגיד "צור ויקי עם שלושה דפים X, Y ו Z. חפש X. תראה שהתוצאה הראשונה היא X."

מעבר ליותר שליטה היתרון הכי גדול של הגישה השניה הוא קביעת המילים. הגדרת היכולות של המערכת. המערכת שלי יודעת לבנות ויקי. היא יודעת לחפש והיא יודעת להציג תוצאות. הגישה השניה מטעינה את הבדיקה במשמעות, והיא דורשת חשיבה, הבנה וכתיבת תשתית (בדרך כלל בתבנית שנקראת Page Object Model9).

בדיקות הן שפה. ושפה טובה הכרחית כדי לתקשר משמעות. בעולם הפיתוח משולב AI בו אנחנו חיים, שפה טובה פותחת ל AI את האפשרות להיות הרבה יותר פרודוקטיבי.
סיכום וובינר - טיפים לעבודה ב VS Code
בוובינר שהתקיים השבוע ביום חמישי פתחנו את VS Code ורצינו להבין איך לשפר את העבודה עם קופיילוט דרכו בדגש על עבודה בצוות. אני מסכם כאן את הדברים המרכזיים שהצגתי לטובת אלה שבאו וגם אלה שלא הצליחו להגיע.

קבצי פרומפט
פרומפט זה הדבר שאנחנו כותבים ל AI. לא משנה כמה אנחנו חכמים בסוף התשובה של ה AI היא תוצאה של הפרומפט שכתבנו יחד עם הקוד בפרויקט. אולי היה נחמד אם ה AI היה מציג לנו "ציון" לפרומפט אחרי כל הודעה שאנחנו שולחים אבל בינתיים זה לא קורה וקשה לפעמים לדעת אם התשובה שקיבלנו היא באמת הטובה ביותר שיש או שאפשר לשפר אותה עם ניסוח אחר.

מפתחים שרוצים לעבוד טוב עם AI יודעים לנסות כמה פרומפטים לכל בעיה והרבה פעמים גם לפתוח שיחות חדשות רק בשביל לנסות פרומפט בלי שיושפע מתחילת השיחה. אפשר גם לפנות ל AI ולבקש שיעזור לנו לשפר את הפרומפט. לאורך זמן ונסיונות אנחנו מגלים כמה טכניקות שעובדות:

1. פרומפטים עובדים טוב יותר כשהם מציינים בבירור מה המטרה ומה תפקיד הסוכן בסיפור - איזה סוג של code review אתה רוצה? זה קוד שמישהו כבר עבר אליו ורק צריך לוודא שלא פספסנו כלום? זאת סקירה ראשונה? אנחנו מסתכלים על סקריפט שרץ פעם בחודש ממכונה בצד או קוד שעונה לאלפי בקשות בשניה? קונטקסט מכוון את הסוכן לתשובה שאנחנו רוצים.

2. פרומפטים עובדים טוב יותר כשהם מציינים איך להציג את התוצאה - אתם רוצים לקבל דוגמת קוד? לתאר תרחיש שבו קוד מסוים יישבר? לכתוב מסמך בדיקות?

בעבודה עם AI אנחנו יכולים לזהות פרומפטים שחוזרים על עצמם לדוגמה:

1. תריץ Code Review על הקומיט האחרון.

2. תסביר מה עושה פונקציה.

3. תכתוב תוכנית בדיקות לפונקציה.

4. תזהה מה המסלול בקוד שגורם לבאג לפי תיאור שהגיע מהתמיכה.

5. נתונה בדיקה שנכשלת, תסביר למה היא נכשלת ותתקן אותה.

6. תעשה Refactor כדי לנקות קוד כפול.

כל אחד מהסעיפים פה יכול לתת תוצאה משמעותית יותר טובה ככל שהפרומפט יהיה טוב יותר אבל ככל שאנחנו מפעילים משהו יותר פעמים כך אנחנו מתעייפים לכתוב את הפרומפט המלא. מה עושים? ב VS Code אפשר ליצור קובץ פרומפט:

1. לוחצים Ctrl+Shift+P ובוחרים Chat: New prompt file

2. בוחרים אם לשמור את קובץ הפרומפט בתוך תיקיית הפרויקט או ב User Data. שמירת פרומפט ב User Data תאפשר לגשת אליו מכל פרויקט שתפתחו מהמחשב שלכם, שמירת הפרומפט בתיקיית הפרויקט תאפשר לכם להוסיף את הפרומפט לגיט וכך הוא יהיה זמין לכל המפתחים האחרים שעובדים אתכם על הפרויקט. את רוב הפרומפטים כדאי לשמור בתיקיית הפרויקט.

3. בוחרים שם לקובץ הפרומפט.

4. כותבים את הפרומפט בקובץ ושומרים.

הפרומפטים נשמרים בתיקיית github/prompts עם סיומת .prompt.md. אחרי שיצרתם קובץ פרומפט אתם יכולים להפעיל אותו מתוך הצ'אט באמצעות כתיבת / ואז שם קובץ הפרומפט (לא צריך את כל השם יש השלמה אוטומטית).

זו דוגמה שראינו לפרומפט שמסביר פונקציה בתוכנית c בפרויקט libuv:

---
agent: agent
---

Please analyze the code below and provide a report with the following sections:

* 1. Functional Analysis *
* **Input Arguments:** detailed breakdown of types, expected ranges, and **ownership semantics** (who frees the memory?). Check for \const\ correctness.
* **Return Values:** Explain all possible return states, including specific error codes, \NULL\ pointers, or \errno\ implications.
* **Intended Use Case:** Infer who calls this function and why, based on the logic (e.g., is it a hot-path utility, a setup function, a blocking I/O call?).

* 2. Critical Code Review (The "Roast") *
* **Edge Cases & Validation:** Does the function handle \NULL\ pointers, zero-length buffers, or integer overflows?
* **Undefined Behavior (UB):** Identify any potential UB (e.g., signed overflows, uninitialized memory, strict aliasing violations).
* **Security:** specific checks for buffer overflows, use-after-free, double-free, or information leaks.

* 3. Performance & Resources *
* **Complexity:** Time and Space complexity (Big O).
* **System Impact:** specific analysis of heap allocations (\malloc\), blocking system calls, or cache locality issues.
* **Concurrency:** Is this function thread-safe? Is it reentrant?

* 4. Refactoring Suggestions *
1
*   Provide a refactored version of the code that fixes identified issues while maintaining the API signature (if possible).


קבצי Instructions
אם יש תוכן שמשותף לכמה פרומפטים ואתם רוצים לכתוב אותו תמיד כשפונים ל AI עבור קבצים מסוימים או קבצים בפרויקט מסוים תוכלו ליצור קובץ instructions:

1. לוחצים Ctrl+Shift+P ובוחרים Chat: New instructions file

2. בוחרים איפה לשמור את הקובץ - אצלכם במשתמש או בפרויקט.

3. בוחרים שם לקובץ.

4. מקלידים את ההוראות ושומרים.

קבצי ההוראות נשמרים בתיקיית instructions עם סיומת .instructions.md. בוובינר ראינו דוגמה לקובץ הוראות לפרויקט C:

---
applyTo: '*.*'
---
Act as a Senior C Systems Engineer and Security Researcher. You're working on a mission-critical library used by many external clients. Reliability, security (memory safety), and high performance are paramount.


כתיבת סוכנים
ל VS Code יש 4 סוכנים מובנים: Agent, Ask, Edit ו Plan. סוכן הוא חיבור בין קובץ הוראות לרשימת כלים שהוא יכול להפעיל - ואנחנו יכולים ליצור סוכנים חדשים למשימות ספציפיות. לדוגמה כדאי לנו לכתוב סוכן שיודע לעשות Code Review ומכיל כבר את תהליך העבודה וההרשאות הספציפיות הנדרשות לביצוע Code Review.

בשביל ליצור קובץ סוכן חדש אני בוחר מתפריט ה Ctrl+Shift+P באופציה New Custom Agent, שוב בוחר בשבילו מיקום וכותב את התוכן בקובץ. לסוכן יש גם הוראות וגם רשימה של כלים ומסך העריכה של VS Code כולל תיבת בחירה כדי לבחור את הכלים. הקובץ עצמו נשמר בתיקיית github/agents בפרויקט עם הסיומת .agent.md. זו דוגמה שראינו לקובץ סוכן שעושה Code Review:

---
name: Code Reviewer
description: An agent that reviews code for quality, style, security, and best practices.
tools: ['vscode', 'read', 'search', 'web', 'agent', 'todo', 'execute']
---
As a "Code Reviewer" agent, your primary function is to meticulously examine provided code snippets or files. Focus on identifying potential bugs, security vulnerabilities, adherence to project coding standards, and overall code quality. Provide actionable feedback and suggest improvements as comments directly in the code or a concise list of recommendations. You must be thorough.

As a "Code Reviewer" agent, your primary function is to meticulously examine provided code.

MANDATORY PROCESS:
1. Run the tests with "npm run test"
2. Perform static code review

If tests cannot be run, explicitly state why.


וזה היה סוכן שיודע רק להריץ את הבדיקות:

---
name: Test Runner
description: A subagent responsible for executing the Vitest test suite, analyzing failures, detecting flaky tests, and flagging potential performance regressions.
tools: ['execute']
---
As a Test Runner agent, your task is to run the project's Vitest test suite
and report results back to the invoking agent.

Responsibilities:
- Execute Vitest with "npm run test"
- Summarize failures with likely causes
- Identify slow or flaky tests
- Call out tests that may mask performance issues
- Do NOT modify code unless explicitly asked

Output should be concise, structured, and suitable for inclusion in a
code review.


אחרי שאני יוצר את הסוכנים המותאמים אישית אני יכול להפעיל אותם מחלון הצ'אט באמצעות שינוי הסוכן הפעיל ואתם תראו אותם מתחת ל-4 הסוכנים המובנים ב VS Code.

הרצת Sub Agents
לפעמים נרצה לבקש מהסוכן לעשות משהו בלי למלא את הקונטקסט בכל התוכן של מה שבוצע ובעצם להסתכל רק על התוצאה של תהליך ארוך. מנגנון כזה נקרא Sub Agents ובוובינר עצמו קצת הסתבכתי איתו. ניסיתי ליצור סוכן Code Review שיפעיל בתור Sub Agent סוכן אחר של Test Runner. גיליתי אחרי הוובינר שבשביל לעשות את זה צריך להפעיל בהגדרות ה VS Code את האופציה Custom Agent In Subagent. אחרי הפעלת האופציה הזו ויציאה מ VS Code והפעלה מחדש הצלחתי להפעיל סוכן של Code Review שיפעיל Sub Agent של Test Runner.

בשביל להפעיל משימה מסוימת בתור Sub agent עלינו לבקש את זה מהסוכן בקובץ ההוראות. בדוגמה הבאה אני מבקש מסוכן ה Code Review לבצע את הרצת הבדיקות בתור סוכן צדדי:

---
name: Code Reviewer
description: An agent that reviews code for quality, style, security, and best practices.
tools: ['vscode', 'read', 'search', 'web', 'agent', 'todo']
---
As a "Code Reviewer" agent, your primary function is to meticulously examine provided code snippets or files. Focus on identifying potential bugs, security vulnerabilities, adherence to project coding standards, and overall code quality. Provide actionable feedback and suggest improvements as comments directly in the code or a concise list of recommendations. You must be thorough.

As a "Code Reviewer" agent, your primary function is to meticulously examine provided code.

MANDATORY PROCESS:
1. Use a sub agent to run the tests with "npm run test"
2. Perform static code review

If tests cannot be run, explicitly state why.


שימו לב לשורה:

1. Use a sub agent to run the tests with "npm run test"


וכן זה הסוכן אצמו שמחליט מה להריץ ב Subagent. אנחנו יכולים רק לבקש. אבל כשזה מצליח והוא מריץ את הבדיקות ב Subagent אז הפלט של הבדיקות לא מלכלך את ה Context Window ומשאיר את הסוכן ממוקד בקוד. זה היה הפלט שהופיע בשיחה:

Since I don't have terminal access to run git commands directly, let me check if there are any staged or unstaged changes first, and also look at the workspace to understand what might have changed recently.

Actually, let me start by:

Running the test suite via subagent (as required)
Then look at what files might be the focus of the latest work
Let me proceed with the mandatory process.


היתרון במבנה זה הוא שאני לא חייב לתת לסוכן ה Code Review את כלי הטרמינל ואני יכול להפריד ולהפעיל כל סוכן עם הכלים שרלוונטים לו.

התקנת שירותי MCP
בוובינר לא הספקנו לדבר על MCP אבל אני כן רוצה לגעת בזה בסיכום. בפרויקט אני יכול ליצור תיקיה בשם .vscode ובתוכה קובץ בשם mcp.json שיכיל את הגדרות שרתי ה MCP של הפרויקט הספציפי. אפשר להגיע לקובץ הזה מתוך Ctrl-Shift-P ולבחור Open Workspace Folder MCP Configuration.

הגדרת שרתי MCP דורשת קצת יותר מחשבה על הארגון ואופן העבודה שלנו עם אנשים אחרים:

1. שרת MCP שמוגדר בפרוטוקול stdio דורש שהתוכנית עצמה תהיה מותקנת לי על המחשב. כשאני מגדיר את הקובץ בהגדרות הפרויקט אני דורש שכל מי שעובד איתי על הפרויקט יתקין את ה MCP הזה.

2. שרת MCP שמוגדר בפרוטוקול http יותר גמיש ויעבוד לכל מי שיפתח את הפרויקט.

לדוגמה אם אני רוצה להוסיף את שרת ה MCP של גיטהאב כדי שאפשר יהיה מתוך VS Code להסתכל על Issues ולבצע פעולות ישירות בגיטהאב אני אכתוב בקובץ:

{
"servers": {
"github": {
"type": "http",
"url": "https://api.githubcopilot.com/mcp/"
}
}
}


בפרויקטים גדולים אני יכול להשתמש ב MCP בשילוב עם סוכנים מותאמים אישית כדי לשלוט באיזה כלים סוכן יוכל להפעיל ואפילו לכתוב כלים ספציפיים עבור אותו פרויקט וסוכנים ספציפיים שישתמשו בכלים אלה.

שוב תודה לכל מי שבאו לוובינר מקווה שנהניתם ולמדתם ותודה מיוחדת לאלה מכם שמשתתפים, מדברים ושואלים - זה מה שהופך את המפגשים למיוחדים ורלוונטים. שבוע הבא נפגש לדבר על איך ללמוד או לשפר מיומנות פיתוח מסוכני קידוד ומה בכלל צריך ללמוד.
2