ToCode
Открыть в Telegram
1 419
Подписчики
Нет данных24 часа
Нет данных7 дней
-530 день
Архив постов
1 419
אקוסיסטם, אינטראופ ותחביר
שלושת השיקולים שלי בבחירת שפת תכנות לפרויקט הבא, ולפי סדר חשיבות:
1. אקוסיסטם - כי הדבר הכי חשוב שאוכל לעבוד עם ספריות מתקדמות ורלוונטיות לעולם התוכן בו אני בונה.
2. אינטראופ - כי אם האקוסיסטם לא מספיק טוב יהיה נחמד להשלים פערים משפות אחרות.
3. תחביר - כי אחרי שיש לי את כל הכלים לבנות את מה שאני רוצה עדיף שאהנה מהכתיבה עצמה.
לפעמים אנחנו חושבים שאינטראופ יעזור להתמודד עם אקוסיסטם חלש או שתחביר טוב יגרום לזה שיהיה לי כיף לכתוב לעצמי את הכלים (או את העטיפות כדי לשפר את האינטראופ). בפרויקטים שכתבתי עד עכשיו זה לא היה המצב.
1 419
}
}
def helper(modules: Map[String, CommunicationModule]): Map[String, CommunicationModule] =
this.pressButton()
process(modules)
def findCycleSize(modules: Map[String, CommunicationModule], end: String): Long =
val relevantModules = influencedBy(modules, inputs, Set(end))
val data = LazyList.iterate(modules)(helper).map { modules =>
modules.view.filterKeys(relevantModules.contains).toMap
}
findCycleIndex(data)
@tailrec
final def process(modules: Map[String, CommunicationModule]): Map[String, CommunicationModule] =
if (this.pendingSignals.nonEmpty) {
val updatedModules = handleSignals(modules)
this.process(updatedModules)
} else {
modules
}
}
sealed trait CommunicationModule {
def name: String
}
case class FlipFlop(name: String, state: Boolean) extends CommunicationModule
case class Conjunction(name: String, state: Map[String, Int]) extends CommunicationModule
case class Broadcaster(name: String) extends CommunicationModule
case class UntypedModule(name: String) extends CommunicationModule
def parseInput(input: Source): (Map[String, CommunicationModule], Map[String, List[String]], Map[String, List[String]]) =
input
.getLines()
.concat(List("button -> broadcaster"))
.map { line => line.split(" -> ") }
.collect { case Array(name, outputs) =>
val outputModules = outputs.split(", ").toList
val module = name match
case "broadcaster" => Broadcaster(name)
case s"%${name}" => FlipFlop(name, false)
case s"&${name}" => Conjunction(name, Map[String, Int]())
case s"${name}" => UntypedModule(name)
(module, outputModules)
}.foldLeft((
Map[String, CommunicationModule](),
Map[String, List[String]](),
Map[String, List[String]]())) { (a, v) =>
val (modules, inputs, outputs) = a
val (newModule, itsOutputs) = v
val newName = newModule.name
val updatedInputs = itsOutputs.foldLeft(inputs) { (inputs, output) =>
inputs.updatedWith(output) {
case Some(existingInputs) => Some(existingInputs :+ newName)
case None => Some(List(newName))
}
}
val updatedOutputs = outputs.updatedWith(newName) {
case Some(existingOutputs) => Some(existingOutputs ++ itsOutputs) // unreachable, because there's only one line per module
case None => Some(itsOutputs)
}
val updatedModules = modules.updated(newName, newModule)
(updatedModules, updatedInputs, updatedOutputs)
}
@main
def day20part1(): Unit =
val (modules, inputs, outputs) = parseInput(Source.fromResource("day20.txt"))
println(modules)
println(inputs)
println(outputs)
val runner = Runner(inputs, outputs)
var round = 0L
var modules_i = modules
1.to(1000).foreach { i =>
runner.pressButton()
modules_i = runner.process(modules_i)
}
println(runner.lowCount)
println(runner.highCount)
println(runner.lowCount * runner.highCount)
@main
def day20part2(): Unit =
val (modules, inputs, outputs) = parseInput(Source.fromResource("day20.txt"))
println(influencedBy(modules, inputs, Set("output")))
val runner = Runner(inputs, outputs)
var m = modules
while (true) {
m = runner.helper(m)
}
// println(runner.findCycleSize(modules, "xf"))
}
סך הכל זה היה אחד התרגילים המעייפים של הסידרה הזו, אבל אולי זה חלק מהעניין עם התרגילים במספרים הגבוהים. הנחמה היחידה היא שאנחנו לקראת הסוף.1 419
import org.neo4j.driver.internal.messaging.request.CommitMessage
import scala.annotation.tailrec
import scala.collection.mutable
import scala.io.Source
import scala.util.chaining._
object aoc2023day20 {
val HIGH_PULSE = 1
val LOW_PULSE = -1
val demoInput: String = """broadcaster -> a, b, c
|%a -> b
|%b -> c
|%c -> inv
|&inv -> a""".stripMargin
val demoInput2: String = """broadcaster -> a
|%a -> inv, con
|&inv -> b
|%b -> con
|&con -> output""".stripMargin
def receive(sender: String, target: CommunicationModule, signal: Int, runner: Runner): CommunicationModule =
target match
case cm @ FlipFlop(name, state) if signal == LOW_PULSE && state =>
runner.sendSignal(LOW_PULSE, name)
cm.copy(state = false)
case cm @ FlipFlop(name, state) if signal == LOW_PULSE && !state =>
runner.sendSignal(HIGH_PULSE, name)
cm.copy(state = true)
case cm @ Conjunction(name, state) =>
val updatedState = runner.inputs(name).map {
case i if i == sender => Map(sender -> signal)
case i if state.contains(i) => Map(i -> state(i))
case i => Map(i -> LOW_PULSE)
}.foldLeft(Map[String, Int]())(_ ++ _)
if (updatedState.values.forall(_ == HIGH_PULSE)) {
runner.sendSignal(LOW_PULSE, name)
} else {
runner.sendSignal(HIGH_PULSE, name)
}
cm.copy(state = updatedState)
case cm @ Broadcaster(name) =>
runner.sendSignal(signal, name)
cm
case _ => target
@tailrec
def influencedBy(modules: Map[String, CommunicationModule],
inputs: Map[String, List[String]],
searching: Set[String] = Set(),
found: Set[String] = Set()): Set[String] =
if (searching.isEmpty) {
found
} else {
val nextModule = searching.head
val nextSearching = searching ++ inputs.getOrElse(nextModule, List()) - nextModule -- found
val nextFound = found + nextModule
influencedBy(modules,
inputs,
nextSearching,
nextFound)
}
def findCycleIndex[T](data: Seq[T]): Long =
data
.scanLeft(Some(Set()): Option[Set[T]]) {
case (Some(seen), newItem) if seen.contains(newItem) => None
case (Some(seen), newItem) => Some(seen + newItem)
case (None, newItem) => None
}.zipWithIndex
.find { (item, index) => item.isEmpty }.get._2 - 1
class Runner(val inputs: Map[String, List[String]], val outputs: Map[String, List[String]]) {
val pendingSignals: mutable.Queue[(String, String, Int)] = mutable.Queue()
var lowCount = 0
var highCount = 0
var buttonPresses = 0
def pressButton(): Unit =
buttonPresses += 1
this.sendSignal(LOW_PULSE, "button")
def sendSignal(signal: Int, from: String): Unit =
val to = outputs(from)
if (signal == LOW_PULSE) {
lowCount += to.size
} else if (signal == HIGH_PULSE) {
highCount += to.size
}
to.foreach { to =>
if (to == "gc" && signal == HIGH_PULSE) {
throw new Exception(s"gc ${this.buttonPresses}")
}
this.pendingSignals.enqueue((from, to, signal))
}
def handleSignals(modules: Map[String, CommunicationModule]): Map[String, CommunicationModule] =
this.pendingSignals.dequeueAll((item) => true).foldLeft(modules) { (modules, message) =>
val (from, to, signal) = message
println(s"${from} - ${if (signal == -1) "low" else "high"} - ${to}")
modules.updatedWith(to) {
case Some(cm) =>
Some(receive(from, cm, signal, this))
case _ =>
println(s"Tried to send signal to untyped module: ${to}")
None1 419
val updatedInputs = itsOutputs.foldLeft(inputs) { (inputs, output) =>
inputs.updatedWith(output) {
case Some(existingInputs) => Some(existingInputs :+ newName)
case None => Some(List(newName))
}
}
val updatedOutputs = outputs.updatedWith(newName) {
case Some(existingOutputs) => Some(existingOutputs ++ itsOutputs) // unreachable, because there's only one line per module
case None => Some(itsOutputs)
}
val updatedModules = modules.updated(newName, newModule)
(updatedModules, updatedInputs, updatedOutputs)
}
בשביל החלק הראשון צריך רק ללחוץ על הכפתור אלף פעמים ולראות כמה סיגנלים חזקים וכמה חלשים נשלחו:
@main
def day20part1(): Unit =
val (modules, inputs, outputs) = parseInput(Source.fromResource("day20.txt"))
println(modules)
println(inputs)
println(outputs)
val runner = Runner(inputs, outputs)
var round = 0L
var modules_i = modules
1.to(1000).foreach { i =>
runner.pressButton()
modules_i = runner.process(modules_i)
}
println(runner.lowCount)
println(runner.highCount)
println(runner.lowCount * runner.highCount)
פיתרון חלק שני
החלק השני היה יותר מורכב ודרש עבודה ידנית. אלה השורות הרלוונטיות מתוך הקלט הספציפי שאני קיבלתי:
&zr -> rx
&gc -> zr
&sz -> zr
&cm -> zr
&xf -> zr
אני רואה ש rx מושפע מ zr, ו zr מושפע מ 4 מודולים אחרים. עכשיו התחילה עבודת החיפוש: בשביל ש rx יקבל אות חלש zr צריך לשלוח לו אות חלש. zr הוא מודול חיבור והוא שולח אות חלש רק כשכל המודולים שמחוברים אליו שולחים אות חזק. עכשיו אנחנו רק צריכים לבדוק מתי gc, sz, cm ו xf מקבלים אות חזק יחד כדי לענות על השאלה.
בשביל להבין מתי מישהו מקבל אות חזק הוספתי את ה Exception הבא לפונקציה sendSignal:
def sendSignal(signal: Int, from: String): Unit =
val to = outputs(from)
if (signal == LOW_PULSE) {
lowCount += to.size
} else if (signal == HIGH_PULSE) {
highCount += to.size
}
to.foreach { to =>
if (to == "gc" && signal == HIGH_PULSE) {
throw new Exception(s"gc ${this.buttonPresses}")
}
this.pendingSignals.enqueue((from, to, signal))
}
אחרי חיפושים גיליתי שכל ה-4 מקבלים אות חזק ממש בלחיצה הראשונה. השאלה הבאה היא מתי בפעם הבאה הם יקבלו אות חזק, כלומר כל כמה לחיצות המצב שלהם מתאפס. את זה כבר היה יותר מסובך לחשב - הטריק היה לזהות מי המודולים שמשפיעים על מודול מסוים בעזרת הפונקציה הבאה:
@tailrec
def influencedBy(modules: Map[String, CommunicationModule],
inputs: Map[String, List[String]],
searching: Set[String] = Set(),
found: Set[String] = Set()): Set[String] =
if (searching.isEmpty) {
found
} else {
val nextModule = searching.head
val nextSearching = searching ++ inputs.getOrElse(nextModule, List()) - nextModule -- found
val nextFound = found + nextModule
influencedBy(modules,
inputs,
nextSearching,
nextFound)
}
לאחר מכן לבנות תת-מפה רק של המודולים האלה ולבנות רשימה של כל המצבים שלהם לאורך הלחיצות, כלומר לבנות רשימה שבמקום הראשון שלה יהיה המצב של המודולים הרלוונטים אחרי לחיצה אחת, במקום השני אחרי שתי לחיצות וכך הלאה:
def findCycleSize(modules: Map[String, CommunicationModule], end: String): Long =
val relevantModules = influencedBy(modules, inputs, Set(end))
val data = LazyList.iterate(modules)(helper).map { modules =>
modules.view.filterKeys(relevantModules.contains).toMap
}
findCycleIndex(data)
אחרי שיש לנו את הרשימה (data) אפשר למצוא מה האינדקס של האיבר הראשון בה שחוזר על עצמו, וזה גודל המעגל שלהם. אחרי שמצאתי את ארבעת המעגלים היה צריך רק לכפול את 4 המספרים כדי למצוא מתי כל ה-4 יסתנכרנו עם אות חזק מה שיגרום ל zr להוציא אות חלש ויסיים את השאלה.
זה קוד הפיתרון המלא בסקאלה:1 419
class Runner(val inputs: Map[String, List[String]], val outputs: Map[String, List[String]]) {
val pendingSignals: mutable.Queue[(String, String, Int)] = mutable.Queue()
var lowCount = 0
var highCount = 0
var buttonPresses = 0
def pressButton(): Unit =
buttonPresses += 1
this.sendSignal(LOW_PULSE, "button")
def sendSignal(signal: Int, from: String): Unit =
val to = outputs(from)
if (signal == LOW_PULSE) {
lowCount += to.size
} else if (signal == HIGH_PULSE) {
highCount += to.size
}
to.foreach { to =>
// if (to == "xf" && signal == LOW_PULSE) {
// throw new Exception(s"xf ${this.buttonPresses}")
// }
this.pendingSignals.enqueue((from, to, signal))
}
def handleSignals(modules: Map[String, CommunicationModule]): Map[String, CommunicationModule] =
this.pendingSignals.dequeueAll((item) => true).foldLeft(modules) { (modules, message) =>
val (from, to, signal) = message
println(s"${from} - ${if (signal == -1) "low" else "high"} - ${to}")
modules.updatedWith(to) {
case Some(cm) =>
Some(receive(from, cm, signal, this))
case _ =>
println(s"Tried to send signal to untyped module: ${to}")
None
}
}
def helper(modules: Map[String, CommunicationModule]): Map[String, CommunicationModule] =
this.pressButton()
process(modules)
def findCycleSize(modules: Map[String, CommunicationModule], end: String): Long =
val relevantModules = influencedBy(modules, inputs, Set(end))
val data = LazyList.iterate(modules)(helper).map { modules =>
modules.view.filterKeys(relevantModules.contains).toMap
}
findCycleIndex(data)
@tailrec
final def process(modules: Map[String, CommunicationModule]): Map[String, CommunicationModule] =
if (this.pendingSignals.nonEmpty) {
val updatedModules = handleSignals(modules)
this.process(updatedModules)
} else {
modules
}
}
חלק מהקוד כאן רלוונטי רק לחלק השני של התרגיל, בגדול מבחינת החלק הראשון הפונקציה הכי חשובה היא sendSignal שמקבלת סיגנל ומודול ושולחת את הסיגנל לכל המודולים שמחוברים לאותו שולח. הפונקציה handleSignals מושכת מהתור את כל הסיגנלים ומטפלת בהם והפונקציה process קוראת ל handleSignals בלולאה כל עוד סיגנלים חדשים מצטרפים לתור. הפונקציה helper מתארת איטרציה אחת של עבודה עם המכונה - היא לוחצת על הכפתור ואז מטפלת בכל הסיגנלים שוב ושוב עד שהתור מתרוקן.
החלק המסובך השני של התרגיל היה לפענח את הקלט. בשביל החלק השני של התרגיל רציתי להחזיק אינדקס כפול גם לשמור את החוטים שיוצאים מכל מודול וגם את החוטים שנכנסים אליו. סך הכל בניתי 3 מפות:
המפה modules מחזיקה לכל אות את המודול שמתאים לה, עם הסטייט הפנימי שלו, השם שלו והסוג שלו.
המפה outputs מחזיקה לכל אות את האותיות שאליהן היא שולחת סיגנלים.
המפה inputs מחזיקה לכל אות את האותיות ששולחות אליה סיגנלים.
הפונקציה הזו בסקאלה בונה את שלושת המפות:
def parseInput(input: Source): (Map[String, CommunicationModule], Map[String, List[String]], Map[String, List[String]]) =
input
.getLines()
.concat(List("button -> broadcaster"))
.map { line => line.split(" -> ") }
.collect { case Array(name, outputs) =>
val outputModules = outputs.split(", ").toList
val module = name match
case "broadcaster" => Broadcaster(name)
case s"%${name}" => FlipFlop(name, false)
case s"&${name}" => Conjunction(name, Map[String, Int]())
case s"${name}" => UntypedModule(name)
(module, outputModules)
}.foldLeft((
Map[String, CommunicationModule](),
Map[String, List[String]](),
Map[String, List[String]]())) { (a, v) =>
val (modules, inputs, outputs) = a
val (newModule, itsOutputs) = v
val newName = newModule.name1 419
פיתרון Advent Of Code 2023 יום 20 בסקאלה
יום עשרים של Advent Of Code הוא אחד התרגילים עם הרבה יותר מדי טקסט, הרבה יותר מדי קוד והרבה יותר מדי עבודה ידנית. בקיצור לא תרגיל שאוכל אי פעם להשתמש בו בקורסים ולא תרגיל שנהניתי ממנו במיוחד. אבל יש גם כאלה בחיים. בואו נראה את המשימה והפיתרון בקצרה. לינק לתרגיל:
https://adventofcode.com/2023/day/20
מה צריך למצוא
נתון קלט שנראה כמו תרשים חומרה:
broadcaster -> a, b, c
%a -> b
%b -> c
%c -> inv
&inv -> a
הקלט מתאר רכיבים ולכל רכיב יש את הלוגיקה שלו. הרכיב שנקרא broadcaster מעביר את האות שהוא מקבל לכל אלה שהוא מחובר אליהם, רכיבים שמתחילים באחוז נקראים Flip Flop ויש להם לוגיקה פנימית ורכיבים שמתחילים באמפרסנד נקראים Conjunction וגם להם יש לוגיקה פנימית שלהם.
רכיב Flip Flop מחזיק מצב פנימי שמתחיל False. הוא יכול לקבל אות "חזק" או "חלש". אם הוא מקבל אות חלש הוא הופך את המצב הפנימי שלו וגם שולח אות חדש. אם הוא התחיל כבוי הוא ישלח אות חזק לכל אלה שהוא מחובר אליהם, ואם הוא התחיל דלוק הוא ישלח אות חלש.
רכיב Conjunction זוכר את האות האחרון שהוא קיבל מכל הרכיבים שנכנסים אליו (אם לא התקבל אות הוא זוכר זרם "חלש"). כשמקבל אות הוא מעדכן את הזיכרון ואז בודק אם כל הקלטים שלו הם זרם "חזק" הוא ישלח זרם חלש, אחרת הוא ישלח זרם חזק.
אה ויש עוד רכיב שנקרא button. הוא מחובר ל broadcaster, לא מופיע בציור וכשלוחצים עליו שולח זרם "חלש" ל broadcaster.
אם הלכתם לאיבוד עד פה אתם לא לבד. לי לקח כמה פעמים לקרוא את ההוראות עד שהבנתי וגם בזמן כתיבת הפיתרון המשכתי לחזור להוראות האלה. נקודה אחרונה היא שצריך לטפל באותו בסדר בו הם נשלחים, לכן אם a שולח אות ל b ול c ואז b שולח אות חדש ל c, אז קודם צריך לטפל בשני האותות ש a שלח ורק אז להמשיך לאות ש b שלח ל c.
בשאלה הראשונה לוחצים על הכפתור אלף פעמים וצריך למצוא כמה אותות חזקים וכמה אותות חלשים נשלחו. בחלק השני צריך למצוא כמה פעמים צריך ללחוץ על הכפתור עד שמודול בשם rx יקבל אות חלש.
פיתרון חלק ראשון בסקאלה
בחלק הראשון רוב העבודה היא לקודד את ההוראות לסקאלה (או לכל שפה אחרת שבחרתם). האתגר היחיד כאן היה הטיפול באותו לפי הסדר, ובשביל זה הגדרתי מחלקה בשם Runner שמגדירה תור הודעות. כל פעם שמישהו שולח אות האות מצטרף לתור ויש פונקציה שמוציאה את כל האותות של סיבוב מסוים מהתור ומטפלת בהם לפי הסדר.
מבחינת מחלקות אלה המחלקות שמחזיקות את המידע על המודולים:
sealed trait CommunicationModule {
def name: String
}
case class FlipFlop(name: String, state: Boolean) extends CommunicationModule
case class Conjunction(name: String, state: Map[String, Int]) extends CommunicationModule
case class Broadcaster(name: String) extends CommunicationModule
case class UntypedModule(name: String) extends CommunicationModule
וזאת הפונקציה שנקראת כשמודול מקבל אות:
def receive(sender: String, target: CommunicationModule, signal: Int, runner: Runner): CommunicationModule =
target match
case cm @ FlipFlop(name, state) if signal == LOW_PULSE && state =>
runner.sendSignal(LOW_PULSE, name)
cm.copy(state = false)
case cm @ FlipFlop(name, state) if signal == LOW_PULSE && !state =>
runner.sendSignal(HIGH_PULSE, name)
cm.copy(state = true)
case cm @ Conjunction(name, state) =>
val updatedState = runner.inputs(name).map {
case i if i == sender => Map(sender -> signal)
case i if state.contains(i) => Map(i -> state(i))
case i => Map(i -> LOW_PULSE)
}.foldLeft(Map[String, Int]())(_ ++ _)
if (updatedState.values.forall(_ == HIGH_PULSE)) {
runner.sendSignal(LOW_PULSE, name)
} else {
runner.sendSignal(HIGH_PULSE, name)
}
cm.copy(state = updatedState)
case cm @ Broadcaster(name) =>
runner.sendSignal(signal, name)
cm
case _ => target
אפשר לראות שמקודדת פה כל הלוגיקה של כל המודולים. באופן כללי אחרי שכתבתי את הפונקציה חשבתי שאולי היה עדיף לבנות את הקוד בגישת Object Oriented ולבנות לוגיקה של כל מודול בתוך המחלקה שלו.
המחלקה Runner היא זאת שמנהלת את תור ההודעות וזה הקוד שלה:1 419
קוד זה אנשים
שאלו אנשים בהאקרניוז מה הקוד הכי טוב שעבדתם עליו?
ציפיתי למצוא שם יצירות אומנות של מקודדים גאונים שישבו במערה וכתבו קוד שנראה כמו קסם. אנשים כמו מרקוס פרסונס (היוצר של מיינקרפט), לינוס טורוולדס או בראם מולנאר.
במקום זה יש אוסף מרשים של אנשים אמיתיים שאומרים: הקוד הטוב ביותר שיצא לי לעבוד עליו נכתב על ידי קבוצה של מתכנתים ברמות מיומנות שונות שדאגו לתקשורת טובה אחד עם השני. אנשים שכיבדו אחד את השני, שהקשיבו, ושהביאו חשיבה ביקורתית ויצירתיות למקום העבודה.
וזה הגיוני - כשיושב מתכנת על מהחלל וצריך לכתוב לבד קוד שכל האנשים בצוות (שזה רק הוא) יכולים להבין זה בעצם לכתוב קוד שהוא יכול להבין באותו רגע, מה שלא מבטיח הרבה מבחינת היכולת לקרוא את הקוד. אבל כשיש לך 3-4 אנשים שיושבים על הקוד וכולם צריכים להבין את הקוד של כל האחרים, אנחנו כבר במשחק אחר מבחינת דרישות התקשורת מה שמוביל כמובן לרמה הרבה יותר גבוהה של התוצאה.
ואת הטיפ הזה קל ליישם אפילו אם אתם ג'וניורים שעובדים על תיק עבודות או פרויקט צד: קחו חבר או חברה שיעבדו אתכם. דאגו שאתם מבינים את כל הקוד שלהם והם את שלכם. דאגו לתעד את הכל כדי שיהיה לשני הצדדים קל לתקשר ולהבין. לאורך זמן תגלו גם שיוצא קוד טוב יותר ויחד איתו גם אתם הופכים למתכנתים טובים יותר.
1 419
כלים או קטגוריות
ג'ודי לי פרסמה רשימה של 20 כלים שכל מתכנת ומתכנתת צריכים להכיר. היא כללה את הכלים הבאים: VS Code, Copilot, Docker, Kubernetes, Postman, Jira, Slack, Figma, Node.js, Webpack, Terraform, VS Code Live Share, Sentry, SonarQube, Prometheus, Grafana, Ansible, Elasticsearch, CircleCI, Tailwind CSS.
כשמקבלים כזאת רשימה יש כמה שאלות שכדאי לשאול לפני שצוללים ללמוד את הכלים (אם בכלל צריך):
1. למי הרשימה מיועדת? איזה סוג מפתחים ירצו את הכלים האלה?
2. מה המטרה של כל כלי? האם יש כלים אחרים עדיפים שאני כבר עובד איתם?
3. איפה ההזדמנות שלי לשפר את הפרודוקטיביות?
4. איפה ההזדמנות שלי ללמוד יכולות חדשות?
אני מתחיל עם השאלה הראשונה - בגלל שהרשימה כוללת פריימוורק ל CSS, כלי לבאנדלינג של קבצי צד-לקוח וכלי עיצוב אני מבין שהיא מיועדת למפתחי צד-לקוח. בגלל כלי ה Deployment וה Monitoring אני מבין שהיא מיועדת ל DevOps ובגלל Node ו Elastic אני מבין שיש פה קריצה למפתחי צד שרת. חיבור כל ה-3 שם אותי במגרש של Full Stack Developers.
עכשיו בתור Full Stack Developer אני יכול להמשיך ולשאול האם אני באמת צריך את הכלים האלה דרך שלושת השאלות הבאות - מה המטרה של הכלי, איפה הזדמנות הלמידה ממנו והאם הוא באמת רלוונטי ל Workflow שלי. סריקה זריזה של הכלים מראה שלכל אחד מהם יש אלטרנטיבות לא פחות טובות וחשובות, ולכן אם היו שואלים אותי הייתי מארגן את הרשימה לפי נושאים:
1. סביבת פיתוח - בין אם זה יהיה VS Code, הכלים של Jetbrains או vim, כדאי למצוא סביבת פיתוח שאתם אוהבים וכל הזמן לחפש סביבות חדשות.
2. יועץ AI - יש לנו את ChatGPT, Claude וכמובן copilot, אבל גם את ה AI של פייסבוק וזה של גוגל ומיסטרל ורבים נוספים. כדאי להכיר כלים כאלה וללמוד איך לשלב אותם בצורה אפקטיבית ב Workflow שלנו.
3. כלי Deployment ו CI/CD - חשוב להבין איך תוכנה עוברת מהמחשב שלנו לשרת או שרתים בענן ואיך היא מנוהלת שם. יש המון כלים שיכולים לעזור בזה כמו Docker, Kubernetes, Ansible, Terraform אבל גם Github Actions ורבים נוספים.
4. כלי ניהול וניטור - יש המון כלים אוטומטיים שעוזרים להבין מה מצב השרתים, החל מכלים שמציגים לוגים דרך אלה שבודקים שהשרתים למעלה ומציגים סטטיסטיקות וגרפים.
5. בסיסי נתונים - יש המון סוגים של בסיסי נתונים מעניינים החל מ Postgresql הסטנדרטי דרך DynamoDB, Mongo, CouchDB, neo4j ורבים נוספים. כדאי להכיר כמה שיותר סוגים של בסיסי נתונים כדי להתאים את בסיס הנתונים למערכת.
6. שפות פיתוח - מפתחי Full Stack רגילים לעבוד ב TypeScript גם בצד שרת וגם בצד לקוח וזה באמת מאוד נוח. היום בנוסף ל node.js יש גם את bun וגם את deno שמריצים TypeScript באופן זה. בנוסף יש עדיין הרבה שפות פיתוח שמתקמפלות ל JavaScript ומאפשרות בנייה של יישומי Full Stack בשפה אחת. נסו למצוא פריימוורקים כאלה ותראו איזה מהם מתאימים לפרויקט שלכם.
כשמסתכלים על כלים בתור קטגוריות אני חושב שיותר קל לקחת כלי שאנחנו כבר היום משתמשים בו ולחפש כלים אחרים במקומו או בנוסף אליו, או לגלות קטגוריות חדשות של דברים שאפילו לא ידענו שקיימים.
1 419
הפיצ'ר שעדיין חסר לי ברידאקס
אחד הפיצ'רים שאני מאוד אוהב בריאייג'נט (גירסת הקלוז'ר של ריאקט) נקרא Cursors. זה קצת כמו useSelector של רידאקס אבל עם אפשרות לעדכן. לדוגמה:
(def state (reagent/atom {:foo {:bar "BAR"}
:baz "BAZ"
:quux "QUUX"}))
;; Now create a cursor
(def bar-cursor (reagent/cursor state [:foo :bar]))
(defn bar-component []
(js/console.log "bar-component is rendering")
[:div @bar-cursor])
התוכנית מגדירה משתנה אחד בשם state שבו מוחזק כל הסטייט של היישום (כמו ה store ברידאקס) ואז עוד משתנה בשם bar-cursor שהוא סוג של מצביע לתוך הסטייט. הקריאה מה cursor, קצת כמו קריאה מ store דרך useSelector יוצרת חיבור בין הקומפוננטה לחלק הזה בסטייט כך שכשהמידע בתוך הנתיב [:foo :bar] ישתנה גם הקומפוננטה תרונדר מחדש, אבל יש פה שני כוחות על שחסרים ברידאקס:
1. ה cursor מיוצר מחוץ לקומפוננטות (לא כזה מלהיב, גם ברידאקס הייתי יכול להגדיר Selector בקובץ נפרד).
2. אפשר לעדכן את ה State דרך ה Cursor.
היכולת השניה היא הדבר הגדול. ברידאקס בשביל להשיג אפקט דומה אני צריך להגדיר Reducer ו Action וכל ה Boilerplate.1 419
הטיית הפעלתנות
הטיית הפעלתנות אומרת שאנחנו מעדיפים להיות בפעולה, ומייחסים יותר ערך לפעולה כשהמאמץ גדול יותר. הטייה זו מסיטה את המבט משתי עובדות חשובות-
1. גם בשביל להישאר במקום צריך לעבוד.
2. כמות המאמץ הדרושה בשביל "ללכת אחורה", "להישאר במקום" או "לזוז קדימה" לא פרופורציונלית לתוצאות.
בן אדם שתקוע באמצע הים יצטרך לעבוד קשה רק בשביל להשאיר את הראש מעל המים, אבל זה לא אומר שהוא מתקרב לחוף. המאמץ הוא בעל ערך רק כשהוא מושקע בדברים שמקדמים אותנו לכיוון המטרות שבחרנו.
בסיפור על הצפרדע שמתבשלת במים שהולכים ונהיים יותר חמים אנחנו אוהבים להדגיש את הסוף - הצפרדע מתה כי היא לא שמה לב שמצבה הולך ונהיה גרוע יותר בזמן שהיא חשבה שהיא עומדת במקום. בני אדם, בניגוד לצפרדעים, תמיד מנסים לשפר את מצבם והאתגר הוא לראות מתי למרות המאמץ (ואולי בגללו) אנחנו דווקא נשארים במקום.
1 419
ומה אם נתחיל מאפס?
חבר שאל אותי על עיצוב מחדש לאתר ישן שכתוב ב Bootstrap. הוא חיפש Theme יפה יותר אבל כל דבר שהוא מצא דרש יותר מדי התאמות. השאלה הבאה שלי היתה "ומה אם נתחיל מאפס?", ופתאום היתה שתיקה שהבהירה שעלינו על משהו.
מסתבר שדווקא אותה בחירה ישנה ב Bootstrap שנעשתה לפני שנים, וכל ההתאמות שעברו על האתר הזה מאז, הן הדבר שסיבך הכי הרבה את העיצוב החדש. המעבר ל Green Field איפשר לו להשתמש ביכולות חדשות של CSS ובטכניקות שהוא מכיר מפרויקטים אחרים עדכניים יותר והפיתוח התחיל לרוץ.
וכן לפעמים יש המון יתרונות בהמשך פיתוח על בסיס תשתית קיימת, ואני יכול להבין למה אנחנו נמשכים תמיד לסטטוס קוו. אין יותר כיף מפשוט להעתיק קוד ממקום אחר ולסיים עוד פיצ'ר. אבל יחד עם הקסם של הסטטוס קוו תמיד חשוב להשאיר בראש את הבחירה - כן אני פה עכשיו. כן זה הפרויקט שאני עובד עליו וזה הקוד והתשתיות עליהם הפרויקט מתבסס. וכן אני בוחר להמשיך להשתמש באותן תשתיות כי זאת הדרך הכי מהירה לפתור את הבעיה הבאה שלי. יום אחד המשפט הזה יפסיק להיות נכון, וחשוב לזהות את הרגע ולא לפחד להתחיל מחדש.
1 419
ואז יום אחד
ואז יום אחד כבר אי אפשר לבנות את הפרויקט על המחשב שלך. ואפילו במכונה וירטואלית אין איך למצוא את הגירסאות הישנות של כל התוכנות שצריכים. ואתה מנסה להיזכר איפה המכונה הישנה ההיא שהריצה אותו פעם אחרונה אבל לא מוצא, ומפה לשם אתה מבין שזה מאוחר מדי. שהפרויקט הזה כבר גמור. שצריך להמשיך הלאה.
או-
ואז ביום אחד הצטרפו אלף משתמשים חדשים למערכת. והם ממש שמחים שמצאו את הפרויקט שלך ורצים לספר לחבריהם, ומפה לשם הצלחת להרוויח ביום מה שבעבודה הרגילה לוקח שנה. ואז ביום אחד אתה מבין שאולי אין טעם להישאר בעבודה וזה זמן טוב להשקיע Full Time בפרויקט. שאולי עלית על הדבר הגדול הבא.
דרמות אוהבות להיראות "פיתאומיות", אבל האמת שזה לא קרה ביום אחד. תהליכים זזים לאט לאט ואז יותר מהר ואז פתאום הרבה יותר מדי מהר. האתגר שלנו הוא לבנות את התהליכים הנכונים ולשבור את השגויים. התוצאות כבר יגיעו עם ריבית בסוף.
Уже доступно! Исследование Telegram 2025 — ключевые инсайты года 
