ToCode
رفتن به کانال در Telegram
1 419
مشترکین
اطلاعاتی وجود ندارد24 ساعت
اطلاعاتی وجود ندارد7 روز
-530 روز
آرشیو پست ها
1 419
cond.comparator match
case "<" if cond.second < compressed.maxS => compressed.copy(maxS = cond.second - 1)
case "<=" if cond.second < compressed.maxS => compressed.copy(maxS = cond.second)
case ">" if cond.second > compressed.minS => compressed.copy(minS = cond.second + 1)
case ">=" if cond.second > compressed.minS => compressed.copy(minS = cond.second)
case _ => compressed
}
@main
def day19part1(): Unit =
val (workflows, items) = parse(Source.fromString(demoWorkflow))
val start = workflows("in")
println(items.filter {
item => process(workflows, start, item)
}.map { item =>
item.x + item.m + item.a + item.s
}.sum)
def count(workflows: Workflows,
name: String,
ruleNumber: Int = 0,
constraints: List[Condition] = List()): Long =
if (name == "R") {
0L
} else if (name == "A") {
sizeOf(compress(constraints))
} else {
val rules = workflows(name)
val rule = rules(ruleNumber)
if (rule.condition.second < MAX_VALUE) {
count(workflows, rule.destination, 0, constraints :+ rule.condition) +
count(workflows, name, ruleNumber + 1, constraints :+ inverseCondition(rule.condition))
} else {
count(workflows, rule.destination, 0, constraints :+ rule.condition)
}
}
@main
def day19part2(): Unit =
val (workflows, items) = parse(Source.fromString(demoWorkflow))
println(count(workflows, "in"))
}
אם יש לכם רעיונות איך לכתוב את זה בסקאלה עם פחות חזרתיות אל תתביישו לשתף בתגובות או בטלגרם.1 419
rule.split(":") match
case Array(destination) => Rule(parseCondition(""), destination)
case Array(condition, destination) => Rule(parseCondition(condition), destination)
// workflow - ex{x>10:one,m<20:two,a>30:R,A}
def parseWorkflow(line: String): (String, Workflow) =
val List(name, workflow) = """(\w+)\{(.*)}""".r.findFirstMatchIn(line).get.subgroups
val rules = workflow.split(",").map(parseRule).toList
(name, rules)
def parseItem(line: String): Item =
val pattern = """\{x=(\d+),m=(\d+),a=(\d+),s=(\d+)}""".r
val List(x, m, a, s) = pattern.findFirstMatchIn(line).get.subgroups
Item(x.toLong,
m.toLong,
a.toLong,
s.toLong)
def parse(source: Source): (Workflows, List[Item]) =
val lines = source.getLines().toList
val workflows = lines
.takeWhile(_.nonEmpty)
.map(parseWorkflow)
.foldLeft(Map[String, Workflow]()) {(workflow, map) =>
val (name, data) = map
workflow ++ Map(name -> data)
}
val items = lines
.dropWhile(_.nonEmpty)
.drop(1)
.map(parseItem)
(workflows, items)
def checkCondition(condition: Condition, item: Item): Boolean =
val op1 = condition.first match
case "x" => item.x
case "m" => item.m
case "a" => item.a
case "s" => item.s
condition.comparator match
case "<" => op1 < condition.second
case "<=" => op1 <= condition.second
case ">" => op1 > condition.second
case ">=" => op1 >= condition.second
def inverseCondition(condition: Condition): Condition =
condition.comparator match
case "<" => condition.copy(comparator=">=")
case "<=" => condition.copy(comparator=">")
case ">" => condition.copy(comparator="<=")
case ">=" => condition.copy(comparator = "<")
def process(workflows: Workflows, current: Workflow, item: Item): Boolean =
current.find(r => checkCondition(r.condition, item)) match
case Some(rule) =>
rule.destination match
case "A" => true
case "R" => false
case next => process(workflows, workflows(next), item)
def sizeOf(condition: CompressedConditions): Long =
(condition.maxX - condition.minX + 1) *
(condition.maxS - condition.minS + 1) *
(condition.maxA - condition.minA + 1) *
(condition.maxM - condition.minM + 1)
def compress(conditions: List[Condition]): CompressedConditions =
conditions.foldLeft(CompressedConditions(1, 4000, 1, 4000, 1, 4000, 1, 4000)) { (compressed, cond) =>
cond.first match
case "x" =>
cond.comparator match
case "<" if cond.second < compressed.maxX => compressed.copy(maxX=cond.second - 1)
case "<=" if cond.second < compressed.maxX => compressed.copy(maxX=cond.second)
case ">" if cond.second > compressed.minX => compressed.copy(minX=cond.second + 1)
case ">=" if cond.second > compressed.minX => compressed.copy(minX=cond.second)
case _ => compressed
case "m" =>
cond.comparator match
case "<" if cond.second < compressed.maxM => compressed.copy(maxM=cond.second - 1)
case "<=" if cond.second < compressed.maxM => compressed.copy(maxM=cond.second)
case ">" if cond.second > compressed.minM => compressed.copy(minM=cond.second + 1)
case ">=" if cond.second > compressed.minM => compressed.copy(minM=cond.second)
case _ => compressed
case "a" =>
cond.comparator match
case "<" if cond.second < compressed.maxA => compressed.copy(maxA=cond.second - 1)
case "<=" if cond.second < compressed.maxA => compressed.copy(maxA=cond.second)
case ">" if cond.second > compressed.minA => compressed.copy(minA=cond.second + 1)
case ">=" if cond.second > compressed.minA => compressed.copy(minA=cond.second)
case _ => compressed
case "s" =>1 419
case "<" if cond.second < compressed.maxX => compressed.copy(maxX=cond.second - 1)
case "<=" if cond.second < compressed.maxX => compressed.copy(maxX=cond.second)
case ">" if cond.second > compressed.minX => compressed.copy(minX=cond.second + 1)
case ">=" if cond.second > compressed.minX => compressed.copy(minX=cond.second)
case _ => compressed
case "m" =>
cond.comparator match
case "<" if cond.second < compressed.maxM => compressed.copy(maxM=cond.second - 1)
case "<=" if cond.second < compressed.maxM => compressed.copy(maxM=cond.second)
case ">" if cond.second > compressed.minM => compressed.copy(minM=cond.second + 1)
case ">=" if cond.second > compressed.minM => compressed.copy(minM=cond.second)
case _ => compressed
case "a" =>
cond.comparator match
case "<" if cond.second < compressed.maxA => compressed.copy(maxA=cond.second - 1)
case "<=" if cond.second < compressed.maxA => compressed.copy(maxA=cond.second)
case ">" if cond.second > compressed.minA => compressed.copy(minA=cond.second + 1)
case ">=" if cond.second > compressed.minA => compressed.copy(minA=cond.second)
case _ => compressed
case "s" =>
cond.comparator match
case "<" if cond.second < compressed.maxS => compressed.copy(maxS = cond.second - 1)
case "<=" if cond.second < compressed.maxS => compressed.copy(maxS = cond.second)
case ">" if cond.second > compressed.minS => compressed.copy(minS = cond.second + 1)
case ">=" if cond.second > compressed.minS => compressed.copy(minS = cond.second)
case _ => compressed
}
def inverseCondition(condition: Condition): Condition =
condition.comparator match
case "<" => condition.copy(comparator=">=")
case "<=" => condition.copy(comparator=">")
case ">" => condition.copy(comparator="<=")
case ">=" => condition.copy(comparator = "<")
וכל התנאים האלה הם מה שהתכוונתי כשכתבתי שסקאלה עבדה לרעתי.
סך הכל קוד הפיתרון המלא בסקאלה הוא:
import scala.io.Source
import scala.util.matching.Regex
object aoc2023day19 {
val MAX_VALUE = 4000
case class Item(x: Long, m: Long, a: Long, s: Long)
case class CompressedConditions(minX: Long, maxX: Long,
minM: Long, maxM: Long,
minA: Long, maxA: Long,
minS: Long, maxS: Long)
case class Condition(first: String, comparator: String, second: Long)
case class Rule(condition: Condition, destination: String)
type Workflow = List[Rule]
type Workflows = Map[String, Workflow]
val demoWorkflowLine: String = "ex{x>10:one,m<20:two,a>30:R,A}"
val demoWorkflow: String = """px{a<2006:qkq,m>2090:A,rfg}
|pv{a>1716:R,A}
|lnx{m>1548:A,A}
|rfg{s<537:gd,x>2440:R,A}
|qs{s>3448:A,lnx}
|qkq{x<1416:A,crn}
|crn{x>2662:A,R}
|in{s<1351:px,qqz}
|qqz{s>2770:qs,m<1801:hdj,R}
|gd{a>3333:R,R}
|hdj{m>838:A,pv}
|
|{x=787,m=2655,a=1222,s=2876}
|{x=1679,m=44,a=2067,s=496}
|{x=2036,m=264,a=79,s=2244}
|{x=2461,m=1339,a=466,s=291}
|{x=2127,m=1623,a=2188,s=1013}""".stripMargin
// condition - x>10
def parseCondition(condition: String): Condition =
val pattern: Regex = "([xmas])([<>])(\\d+)".r
condition match
case "" => Condition("x", "<=", MAX_VALUE)
case pattern(a, b, c) => Condition(a, b, c.toLong)
// rule - x>10:one
def parseRule(rule: String): Rule =1 419
// workflow - ex{x>10:one,m<20:two,a>30:R,A}
def parseWorkflow(line: String): (String, Workflow) =
val List(name, workflow) = """(\w+)\{(.*)}""".r.findFirstMatchIn(line).get.subgroups
val rules = workflow.split(",").map(parseRule).toList
(name, rules)
def parseItem(line: String): Item =
val pattern = """\{x=(\d+),m=(\d+),a=(\d+),s=(\d+)}""".r
val List(x, m, a, s) = pattern.findFirstMatchIn(line).get.subgroups
Item(x.toLong,
m.toLong,
a.toLong,
s.toLong)
def parse(source: Source): (Workflows, List[Item]) =
val lines = source.getLines().toList
val workflows = lines
.takeWhile(_.nonEmpty)
.map(parseWorkflow)
.foldLeft(Map[String, Workflow]()) {(workflow, map) =>
val (name, data) = map
workflow ++ Map(name -> data)
}
val items = lines
.dropWhile(_.nonEmpty)
.drop(1)
.map(parseItem)
(workflows, items)
הפונקציה המעניינת הבאה מקבלת פריט ותנאי ובודקת אם הפריט מתאים לתנאי:
def checkCondition(condition: Condition, item: Item): Boolean =
val op1 = condition.first match
case "x" => item.x
case "m" => item.m
case "a" => item.a
case "s" => item.s
condition.comparator match
case "<" => op1 < condition.second
case "<=" => op1 <= condition.second
case ">" => op1 > condition.second
case ">=" => op1 >= condition.second
ואחריה אפשר היה לכתוב פונקציית המשך רקורביסית שבודקת אם הפריט מסתיים ב A או ב R:
def process(workflows: Workflows, current: Workflow, item: Item): Boolean =
current.find(r => checkCondition(r.condition, item)) match
case Some(rule) =>
rule.destination match
case "A" => true
case "R" => false
case next => process(workflows, workflows(next), item)
זה מספיק בשביל החלק הראשון:
def day19part1(): Unit =
val (workflows, items) = parse(Source.fromResource("day19.txt"))
val start = workflows("in")
println(items.filter {
item => process(workflows, start, item)
}.map { item =>
item.x + item.m + item.a + item.s
}.sum)
הרעיון המרכזי של החלק השני הוא הפונקציה הבאה:
def count(workflows: Workflows,
name: String,
ruleNumber: Int = 0,
constraints: List[Condition] = List()): Long =
if (name == "R") {
0L
} else if (name == "A") {
sizeOf(compress(constraints))
} else {
val rules = workflows(name)
val rule = rules(ruleNumber)
if (rule.condition.second < MAX_VALUE) {
count(workflows, rule.destination, 0, constraints :+ rule.condition) +
count(workflows, name, ruleNumber + 1, constraints :+ inverseCondition(rule.condition))
} else {
count(workflows, rule.destination, 0, constraints :+ rule.condition)
}
}
זו פונקציה רקורסיבית שעוברת על כל המסלולים שמגיעים ל A ו"אוספת" את כל התנאים שמגיעים לשם - לדוגמה אם היה תנאי x<50 אז נאסוף אותו כדי לדעת שבמסלול מסוים אנחנו יכולים להתקדם רק אם התנאי הזה מתקיים. כל "כלל" בתוכנית מייצר שתי אפשרויות, אפשרות אחת אם הכלל הזה אמיתי ואפשרות שניה אם הוא שקרי. אז לדוגמה אם יש לנו את השורה:
crn{x>2662:A,R}
אז אני יודע לחלק אותו לשני מסלולים - במסלול אחד x באמת קטן מ 2662 ואז נגיע ל A, ובמסלול שני x גדול או שווה ל 2662 ואז מגיעים ל R. זה הסיפור של החיבור הרקורסיבי שמופיע בפונקציה.
וכן בשביל שהפונקציה תעבוד היא צריכה את המימושים של פונקציות העזר:
def sizeOf(condition: CompressedConditions): Long =
(condition.maxX - condition.minX + 1) *
(condition.maxS - condition.minS + 1) *
(condition.maxA - condition.minA + 1) *
(condition.maxM - condition.minM + 1)
def compress(conditions: List[Condition]): CompressedConditions =
conditions.foldLeft(CompressedConditions(1, 4000, 1, 4000, 1, 4000, 1, 4000)) { (compressed, cond) =>
cond.first match
case "x" =>
cond.comparator match1 419
פיתרון Advent Of Code יום 19 בסקאלה
אני כבר לא זוכר כמה זמן עבר מאז חידת ה Advent Of Code הקודמת שפתרתי כאן בסקאלה, אבל יצא שתוכניות השתנו היום וסוף סוף היה לי זמן להמשיך לפרק הבא. למי שעוקב אנחנו בחידה 19 מתוך 25 מה שאומר שיש סיכוי לא רע שעד סוף השנה אצליח לסיים את כל ה 25 חידות של שנה שעברה.
האתגר
היום סקאלה עבדה נגדי או שאולי פשוט השתמשתי בה לא נכון. הקלט שלנו נראה כך:
px{a<2006:qkq,m>2090:A,rfg}
pv{a>1716:R,A}
lnx{m>1548:A,A}
rfg{s<537:gd,x>2440:R,A}
qs{s>3448:A,lnx}
qkq{x<1416:A,crn}
crn{x>2662:A,R}
in{s<1351:px,qqz}
qqz{s>2770:qs,m<1801:hdj,R}
gd{a>3333:R,R}
hdj{m>838:A,pv}
{x=787,m=2655,a=1222,s=2876}
{x=1679,m=44,a=2067,s=496}
{x=2036,m=264,a=79,s=2244}
{x=2461,m=1339,a=466,s=291}
{x=2127,m=1623,a=2188,s=1013}
והוא מחולק לשני בלוקים. בבלוק התחתון יש רשימה של פריטים, לכל פריט יש 4 מאפיינים המסומנים באותיות x, m, a, s. הבלוק השני הוא "תוכנית" שמתחילה בשורה שמתחילה ב in וכוללת רשימת תנאים ותוצאות. כשיש לך פריט שמתאים לתנאי ממשיכים לתוצאה שלידו. התוצאה A אומרת שהפריט "התקבל" והתוצאה R אומרת שהפריט לא התקבל. אלה כמה דוגמאות לזרימה של פריטים בתוך תוכניות:
{x=787,m=2655,a=1222,s=2876}: in -> qqz -> qs -> lnx -> A
{x=1679,m=44,a=2067,s=496}: in -> px -> rfg -> gd -> R
{x=2036,m=264,a=79,s=2244}: in -> qqz -> hdj -> pv -> A
{x=2461,m=1339,a=466,s=291}: in -> px -> qkq -> crn -> R
{x=2127,m=1623,a=2188,s=1013}: in -> px -> rfg -> A
אז אנחנו רואים לדוגמה בפריט הראשון שהוא מתחיל ב in, ה s שלו גדול מ 1351 ולכן לא ממשיך ל px אלא ל qqz. שם ה s שלו גדול מ 2770 ולכן ממשיך ל lnx ובגלל שה m שלו גדול מ 1548 הוא מגיע ל A ופה מסיימים.
השאלה הראשונה היא לגלות איזה פריטים מגיעים ל A ולחבר את כל ה a, x, m ו s-ים שלהם.
בשאלה השנייה עלינו לדמיין שכל מאפיין יכול לקבל ערכים בין 1 ל 4000, ולגלות כמה פריטים תיאורטית סך הכל (בלי קשר לרשימה שהם נתנו) יכולים להגיע ל A.
פיתרון חלק ראשון
הגדרתי המון טיפוסים בסקאלה כדי שיעזרו לי להתמודד עם כל הטקסט של החידה:
import scala.io.Source
import scala.util.matching.Regex
object aoc2023day19 {
val MAX_VALUE = 4000
case class Item(x: Long, m: Long, a: Long, s: Long)
case class CompressedConditions(minX: Long, maxX: Long,
minM: Long, maxM: Long,
minA: Long, maxA: Long,
minS: Long, maxS: Long)
case class Condition(first: String, comparator: String, second: Long)
case class Rule(condition: Condition, destination: String)
type Workflow = List[Rule]
type Workflows = Map[String, Workflow]
val demoWorkflowLine: String = "ex{x>10:one,m<20:two,a>30:R,A}"
val demoWorkflow: String = """px{a<2006:qkq,m>2090:A,rfg}
|pv{a>1716:R,A}
|lnx{m>1548:A,A}
|rfg{s<537:gd,x>2440:R,A}
|qs{s>3448:A,lnx}
|qkq{x<1416:A,crn}
|crn{x>2662:A,R}
|in{s<1351:px,qqz}
|qqz{s>2770:qs,m<1801:hdj,R}
|gd{a>3333:R,R}
|hdj{m>838:A,pv}
|
|{x=787,m=2655,a=1222,s=2876}
|{x=1679,m=44,a=2067,s=496}
|{x=2036,m=264,a=79,s=2244}
|{x=2461,m=1339,a=466,s=291}
|{x=2127,m=1623,a=2188,s=1013}""".stripMargin
ואז המשכתי לכתוב את כל הפונקציות שמפענחות את הקלט:
// condition - x>10
def parseCondition(condition: String): Condition =
val pattern: Regex = "([xmas])([<>])(\\d+)".r
condition match
case "" => Condition("x", "<=", MAX_VALUE)
case pattern(a, b, c) => Condition(a, b, c.toLong)
// rule - x>10:one
def parseRule(rule: String): Rule =
rule.split(":") match
case Array(destination) => Rule(parseCondition(""), destination)
case Array(condition, destination) => Rule(parseCondition(condition), destination)1 419
מה הופך מימוש לקשה לקריאה
לא משנה אם אנחנו מדברים על JavaScript, על פייתון, על פרל או על ראסט, יש כמה מאפיינים שיכולים להפוך מימוש של אלגוריתם לקשה במיוחד לקריאה. אנטון זייאנוב ריכז מימושים של UUID7 ב 32 שפות בקישור כאן:
https://antonz.org/uuidv7/
ואני חשבתי שזו הזדמנות מצוינת לחפש את הקושי.
מה אנחנו בונים
המבנה של UUID7 כולל 48 ביטים של תווית זמן, אחרי זה 4 ביטים של גירסה (הקבוע 7), אחריהם 12 ביטים אקראיים, שני ביטים שהם 10 - שבגלל שאנחנו מייצגים את הערך כמחרוזת ותו במחרוזת הוא 4 ביטים יכולים להיות כל אחד מהערכים 8, 9, a, b ואז עוד 62 ביטים אקראיים. אני מסכים זה קצת מסורבל אבל מה שחשוב זה איך מתרגמים אותו לקוד.
מה הופך את המימוש לקשה לקריאה
זה לא משנה על איזה שפת תכנות אנחנו מדברים, ואני ממליץ לכם להעיף מבט בכל המימושים אצל אנטון, הקושי בקריאה הוא תמיד באותם מקומות.
האתגר הראשון הוא העתקת המערך. שימו לב לקטע הבא ב JavaScript:
* timestamp *
value[0] = (timestamp >> 40) & 0xFF
value[1] = (timestamp >> 32) & 0xFF
value[2] = (timestamp >> 24) & 0xFF
value[3] = (timestamp >> 16) & 0xFF
value[4] = (timestamp >> 8) & 0xFF
value[5] = timestamp & 0xFF
למי שמכיר את האופרטור >> אין בעיה לראות כאן שמעתיקים כל פעם קטע אחר מתוך timestamp ל value, אבל האמת שאם את כל הבלוק הזה היינו מעבירים לפונקציה עם שם כמו ArrayCopy הוא היה יותר קריא. נשווה עם Java:
System.arraycopy(timestamp.array(), 2, value, 0, 6);
אפילו בלי להכיר Java ברור שהמטרה של השורה היא להעתיק דברים ממערך אחד לאחר, ובאופן כללי אפשר לסכם שקוד שנותן שמות לפעולות יהיה יותר קל להבנה מקוד שפשוט כותב אותן.
אתגר שני הוא השמות של הפונקציות המובנות בשפה. השורה הזאת ב PHP ממחישה את הסיפור:
// current timestamp in ms
$timestamp = intval(microtime(true) * 1000);
הקוד לא מספר מה היחידה ולכן צריך הערה שתסביר שמדובר במילי שניות. אני משווה את זה לסי שארפ:
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
וברור שבגירסת הסי שארפ ההערה מיותרת.
עוד אחד מאותו סוג הוא יצירת הערכים האקראיים. ככה זה נראה בקוטלין:
private val random = SecureRandom()
וב Ruby:
value = SecureRandom.random_bytes(16).bytes
וב Elixir:
value = :crypto.strong_rand_bytes(16) |> :binary.bin_to_list()
לעומת Julia:
value = rand(UInt8, 16)
או PHP:
$value = random_bytes(16);
או אפילו go:
_, err := rand.Read(value[:])
שימו לב איך בשלושת הראשונים ברור שמדובר במספרים אקראיים חזקים המתאימים לשימוש באבטחת מידע רק לפי השם של הפונקציה, ובשלושת האחרונים הייתי נזהר לכתוב כזה קוד בלי לבדוק כמה פעמים בתיעוד.
סיפור אחרון הוא שמות למשתנים גם בחישובי ביניים. הדבר הזה מאוד בולט בקלוז'ר:
(concat
;; timestamp
(map byte (.toByteArray (biginteger (System/currentTimeMillis))))
;; version
[(bit-or (bit-and (first rand-array) 0x0F) 0x70)]
[(nth rand-array 1)]
;; variant
[(bit-or (bit-and (nth rand-array 2) 0x3F) 0x80)]
(drop 3 rand-array))))
אם היינו מפרידים את החישוב של כל אחד מהחלקים לאיזה let בתחילת הפונקציה היינו מקבלים concat יותר קריא בלי הצורך בהערות:
(concat
timestamp
version
rand_a
variant
rand_b)
סך הכל קריאת המימוש הזה ב 32 שפות תכנות רק הבהירה לי שהדרך לקוד קריא לא קשורה כל כך לתחביר השפה אלא יותר לאופן בו אנחנו משתמשים בה. בחירת שמות טובים, חלוקה ברורה לפונקציות והפרדה לרמות אבסטרקציה תמיד יעזרו לנו לבנות קוד שיהיה קל יותר לאחרים לקרוא.1 419
חיפוש תמונות ב pixabay מתוך סקאלה
בזכות ChatGPT לקח לי שלוש דקות להפוך את התיעוד של ה API של pixabay להגדרות טיפוסים בסקאלה, לפחות עבור התשובות שמקבלים מה API. זה כבר שיפור משמעותי לעומת החיים לפני ChatGPT בהם הייתי צריך לבנות לבד את הגדרות הטיפוסים או לוותר על Type Safety. אבל לצערי זה עדיין לא מספיק.
שתי הבעיות שנשארו לי עם כתיבת הגדרות הטיפוסים לבד הן:
1. אם דברים ישתנו בעתיד אני אצטרך לעקוב אחרי השינויים ולעדכן את הקוד.
2. בגלל אילוצי החיים, בניתי רק הגדרות טיפוסים לדברים שהייתי צריך. הרחבות עתידיות יכריחו אותי לעבור שוב בתיעוד ולרענן את הקוד.
הצד השני של הסיפור הוא שספריה מסודרת יכולה להיות לא מספיק מתוחזקת, כך שפיצ'רים שזמינים ב API לא יהיו זמינים עבור הקוד שלנו רק בגלל הבחירה להשתמש בספריית צד-שלישי ולא לגשת דרך ממשק REST.
בינתיים וכל עוד אני כותב בסקאלה הסיכוי שמישהו ידאג לי לספריה מסודרת רק יורד עם כל יום שעובר. אם במקרה אתם מגיעים לפה וצריכים קוד סקאלה לעבודה עם פיקסאביי מוזמנים להשתמש בזה בתור בסיס:
package dictionary.images
import com.typesafe.scalalogging.Logger
import common.ClaudeSyncClient.getClass
import io.circe.generic.auto.*
import io.circe.parser.*
import sttp.client3.*
import sttp.client3.circe.*
import sttp.model.Uri
import scala.util.chaining.*
case class ImageHit(id: Int,
pageURL: String,
\type\: String,
tags: String,
previewURL: String,
previewWidth: Int,
previewHeight: Int,
webformatURL: String,
webformatWidth: Int,
webformatHeight: Int,
largeImageURL: String,
imageWidth: Int,
imageHeight: Int,
imageSize: Int,
views: Int,
downloads: Int,
collections: Int,
likes: Int,
comments: Int,
user_id: Int,
user: String,
userImageURL: String)
case class ApiResponse(total: Int,
totalHits: Int,
hits: List[ImageHit])
object Pixabay {
private val API_KEY: String = System.getenv("PIXABAY_API_KEY")
val client: SimpleHttpClient = SimpleHttpClient()
val logger: Logger = Logger(getClass)
def searchImages(query: String): ApiResponse =
val queryParams = Map(
"key" -> API_KEY,
"q" -> query,
"image_type" -> "photo",
"page" -> "1"
)
val baseUrl = uri"https://pixabay.com/api/"
val uriWithParams: Uri = uri"$baseUrl?$queryParams"
println(uriWithParams)
basicRequest
.get(uriWithParams)
.response(asJson[ApiResponse])
.pipe(client.send)
.body match
case Right(response) => response
case Left(err) =>
logger.error("Pixabay error: ", err)
throw Exception(s"Error getting images from pixabay", err)
@main
def testImageSearchPixabay(): Unit =
val response = searchImages("raisin")
println(response.hits.head.previewURL)
}
נ.ב. מכל מיני סיבות של סקאלה וקסמים של JSON שאני לא מספיק מבין, בשביל שהקוד יעבוד יש צורך להוסיף את השורה הבאה ל build.sbt. אם מישהו מהקוראים או הקוראות יודעים לספר לי יותר על זה אשמח לשמוע בתגובות:
scalacOptions ++= Seq("-Xmax-inlines", "100")1 419
גרמלין נשך אותי
הייתי צריך לראות את זה בא. הייתי צריך לחשוב על זה כשכתבתי את הקוד. אבל באמת שבאותו רגע לא העליתי על דעתי מה שהולך להגיע. זה הסיפור על גרמלין מרושע ועל החשיבות של בניית ממשק לא מבלבל.
אני מתחיל עם בניית גרף חדש בזיכרון. השפה היא סקאלה:
import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerTransactionGraph
import org.apache.tinkerpop.gremlin.process.traversal.AnonymousTraversalSource.traversal
import scala.jdk.CollectionConverters._
val graph = TinkerTransactionGraph.open
val g = traversal.withEmbedded(graph)
ואז יוצר כמה פריטים:
g.addV("item").next()
g.addV("item").next()
g.addV("item").next()
וגם יכול להדפיס את המידע עליהם עם השאילתה:
scala> g.V().hasLabel("item").limit(2).elementMap().toList.asScala
val res21: scala.collection.mutable.Buffer[java.util.Map[Object, Object]] = Buffer({id=0, label=item}, {id=1, label=item})
עכשיו לחלק שהפתיע אותי (למרות שבמבט שני על הקוד האשמה כולה עליי). נניח שאני רוצה לפצל את השאילתה לשני חלקים, קודם לקחת את כל ה IDs של הצמתים שיצרתי ורק אחר כך להתחיל מעבר חדש על הצמתים לפי ה ID ולהדפיס את המידע. אני יכול להתפתות ולכתוב:
scala> val allItems = g.V().hasLabel("item").limit(2).toList.asScala.toList
val allItems: List[org.apache.tinkerpop.gremlin.structure.Vertex] = List(v[0], v[1])
scala> g.V(allItems: _*).elementMap().toList
val res22: java.util.List[java.util.Map[Object, Object]] = [{id=0, label=item}, {id=1, label=item}]
וזה עובד ממש טוב! אבל זה שקר, כי נשים לב מה קורה אם אני מנסה לשנות קצת את השאילתה הראשונה ולבחור פריטים שאינם קיימים:
scala> val allItems = g.V().hasLabel("x").limit(2).toList.asScala.toList
val allItems: List[org.apache.tinkerpop.gremlin.structure.Vertex] = List()
scala> g.V(allItems: _*).elementMap().toList
val res23: java.util.List[java.util.Map[Object, Object]] = [{id=0, label=item}, {id=1, label=item}, {id=2, label=item}]
השאילתה הראשונה אכן מבינה שאין פריטים עם התווית x ומחזירה רשימה ריקה, אבל הפקודה השנייה מופעלת על רשימה ריקה שזה בדיוק כמו להפעיל g.V() בלי פרמטרים, שזאת פקודה שרצה על כל הצמתים בגרף, ולכן אני מקבל את המידע על כל הצמתים.
והלקח מהסיפור - לא להשתמש ב spread operator, או לפחות אם משתמשים בו תמיד לבדוק מה קורה כשהרשימה ריקה.1 419
כבר לא
מה יותר מהיר, שאילתה אחת עם JOIN או מספר שאילתות וחיבור הנתונים בזיכרון?
מה יותר מהיר, חיבור כל קבצי ה JavaScript לקובץ אחד ארוך או שליחת 50 פניות לשרת ל 50 קבצים שונים?
מה יותר מהיר, הוספה או עדכון?
אחרי מספיק שנים בתעשייה יש לנו עולם שלם של מידע שגילינו בדרך הקשה. מידע בו אנחנו שמחים להשתמש לפרויקט הבא ולקחת כמובן מאליו, כי כבר שילמנו את "מחיר" הלמידה שלו.
מתכנתים טובים זוכרים שבתעשייה שלנו כל המידע שנלמד בעמל רב הופך מהר מאוד ללא רלוונטי. מה שחשוב לקחת איתנו לפרויקט הבא הוא תהליך הגילוי, תשומת הלב לפרטים והיכולת לשאול שאלות ולחפש תשובות. כן אותו "מחיר" ששילמנו על הלמידה הוא בעצם הדבר שאנחנו צריכים ללמוד.
1 419
חידת JavaScript - מחרוזת למספר
מה מחזיר הקוד הבא? למה?
['1', '2'].map(parseInt)
פיתרון
הקוד מחזיר את המערך:
[ 1, NaN ]
אני מבין למה 1 בתא הראשון, אבל מאיפה הגיע ה NaN? ולמה parseFloat כן עובד שם?
> parseInt(2)
2
> ['1', '2'].map(parseFloat)
[ 1, 2 ]
הסבר
הסיפור פשוט אבל מזכיר ש JavaScript תמיד תישאר JavaScript. הפונקציה parseInt מקבלת למעשה שני פרמטרים, פרמטר ראשון הוא המחרוזת שצריך להפוך למספר ופרמטר שני הוא בסיס הספירה. כשמריצים אותה בתוך map הפרמטר הראשון שנשלח הוא הערך מהמערך והשני הוא האינדקס שלו, וכך מה שבאמת ניסינו להפעיל שם היה:
> parseInt('2', 1)
NaN
וזה כבר NaN כי אי אפשר לפענח את המחרוזת 2 כמספר בבסיס 1. ההתנהגות מוסברת בפיסקה הבאה מתוך התיעוד של parseInt:
> if it's nonzero and outside the range of 2, 36 after conversion, the function will always return NaN
וככה זה נראה על כל המערך של בסיסי ספירה עד 99:
> Array(100).fill(0).map((_, i) => \${i}\).map(parseInt)
[
0, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, 10, 12,
14, 16, 18, 20, 22, 24, 26, 28, 40, 43, 46, 49,
52, 55, 58, 61, 64, 67, 90, 94, 98, 102, 106, 110,
114, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN,
NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN,
NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN,
NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN,
NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN,
NaN, NaN, NaN, NaN
]
ומה לגבי parseFloat? נו, היא לא מקבלת פרמטר שני ולכן מחזירה תמיד את התוצאה הנכונה.
נ.ב. גם טייפסקריפט לא התלוננה על קוד כזה כך שלפעמים אין תחליף להבנה.1 419
למי חשוב ה git diff?
דרך אחת לחשוב על עבודה עם גיט היא שאנחנו עובדים עם גיט כי זה מה שהפרויקט מכתיב. אם היה אפשר הייתי מעלה את הקבצים לאיזה דרופבוקס, אבל זאת לא בחירה שלי. ככה מנהלים את העבודה בחברה.
דרך שנייה לחשוב על גיט היא שאנחנו משתמשים בגיט כדי "לשמור" גירסאות. אני יודע מה זה גירסה וכל פעם שיש לי משהו שעובד אני דוחף אותו לשרת ונותן לו שם. בזכות גיט אני יכול לדפדף בין כל הגירסאות שיצרתי בשביל לקבל רעיונות, להיזכר או להציל את המצב כשפתאום הקוד מפסיק לעבוד.
דרך שלישית לחשוב על גיט היא בתור כלי לתקשורת. לא תקשורת בין מחשבים אלא בין אנשים - גיט מאפשר להוסיף עוד רמה של מידע על הקוד, שמסבירה לא רק מה הדבר הזה עושה אלא גם למה זה עושה את מה שזה עושה. מאיפה זה הגיע, איך החלטתי לבחור דווקא בשיטה הזאת, ולשמור את המידע הזה ליד הקוד בצורה מסוימת אבל לא בתוכו.
כשאנשים כותבים בהודעת הקומיט Push Daily Work הם לא מספרים לנו מה הם עשו אלא איך הם חושבים על גיט. ואם אנחנו רוצים לקבל יותר פירוט כדאי קודם כל לדבר ולהסכים על התפקיד של גיט בתהליך הפיתוח אצלנו בחברה.
1 419
מה לפני איך
גם המתכנת הכי טוב בעולם לא יצליח להתקדם בלי לדעת לאן. זה כנראה נכון בכל תחום אבל במיוחד בפיתוח תוכנה - כשאנחנו צריכים כל הזמן לבחור בין שיטות עבודה וכל בחירה מגיעה עם משמעויות והשפעה על עתיד המערכת.
בשביל ליישם גישה מונחית מטרה לפיתוח תוכנה נרצה לייצר שני מנגנונים: מנגנון אחד שעוזר לנו להבין איזה פיצ'רים לפתח ובאיזה כלים להשתמש, ומנגנון בקרה שעוזר לנו להבין איפה צדקנו ואיפה טעינו כדי שנוכל לקבל החלטות טובות יותר פעם הבאה.
במנגנון הראשון הייתי מוסיף לכל דרישה מוצרית (בין אם הגיעה מ Product ובין דרישה פנימית מהפיתוח):
1. מי צריך את הפיצ'ר הזה?
2. מה הבעיה שהפיצ'ר אמור לפתור לאותו לקוח?
3. מה אותו לקוח עושה היום?
4. איך הפיצ'ר ישנה את ה Workflow של הלקוח?
5. איזה פיצ'רים אחרים עשויים להשפיע על אותה בעיה?
במנגנון השני הייתי מוסיף לתהליך העבודה פעם בחצי שנה פגישה מסודרת בה עוברים על הפיצ'רים שפיתחנו והבעיות שפתרנו, מוודאים שהפיצ'רים שבנינו באמת פותרים ללקוחות את הבעיות שהם אמורים לפתור ואם לא מנסים להבין איפה היו הטעויות ומה אפשר לעשות טוב יותר בעתיד.
בואו ניקח דוגמה קצרה כי הסיפור הזה חשוב. נניח שאני בונה מערכת לאיחסון תמונות ואני מזהה שמשתמשים בטעות מוחקים לעצמם תמונות חשובות ואז מתקשרים לתמיכה כדי שישחזרו להם את התמונות. אני יכול לקבל רעיון לבנות מנגנון של "סל מיחזור", בו כל תמונה שנמחקת תעבור לסל המיחזור ורק אחרי 30 ימים שם היא תימחק סופית מהמערכת וכך למשתמשים תהיה הזדמנות להחזיר את התמונה. כשאני מברר את ה"מה" לפני ה"איך" אני קודם כל מנסה להבין כמה אנשים פנו לתמיכה בשביל שיחזירו להם את התמונות שבטעות נמחקו? מי האנשים האלה? האם הם משתמשים חדשים במערכת או וותיקים? האם משתמשים בתוכנית החינמית או לקוחות משלמים? האם יש כבר גיבוי לתמונות? למה אין מנגנון שמאפשר להם לשחזר תמונות מגיבוי? כמה זמן אחרי שהתמונות נמחקו הם פנו לתמיכה? האם זה אותם משתמשים שפונים או כל פעם משתמשים אחרים? האם אפשר לדבר איתם?
ככל שיש לי יותר קונטקסט אני יכול לבנות מערכת שיותר תתאים לבעיה הספציפית ולהבין כמה זמן כדאי לשמור תמונות באותו סל מיחזור, איזה סוג אינדיקציה להציג למשתמשים לגבי סל המיחזור, איך לתקשר את היחסים בין סל המיחזור לגיבוי השוטף וכל החלטה אחרת שנצטרך לקבל לאורך הדרך.
צריך להבין גם שיש עלות לכל פיצ'ר. בדוגמה של סל המיחזור אולי משתמשים מצפים שהתמונה באמת תימחק כשהם מוחקים אותה מהמערכת ואולי הם יופתעו לרעה אם יום אחד יפרצו לי לשרתים ויקחו את כל התמונות שכביכול "נמחקו". ברור שיש פה שאלה מוצרית אבל אי אפשר לנתק אותה מההיבט הטכני של הפיתוח.
ככל שחברנו ה AI הולך לעזור יותר בכתיבת קוד, כך היכולת להסתכל על ה"מה" לפני ה"איך" הופכת להיות חלק בלתי נפרד מהעבודה שלנו.
اکنون در دسترس! پژوهش تلگرام ۲۰۲۵ — مهمترین بینشهای سال 
