ch
Feedback
ToCode

ToCode

前往频道在 Telegram

טיפים קצרים למתכנתים מאת ינון פרק

显示更多
1 420
订阅者
+124 小时
+17
-430
帖子存档
ToCode
1 420
טעות באבסטרקציה כשאני מלמד Generators בפייתון אני אוהב להתחיל עם הפונקציה הזאת שמחשבת מספר בסידרת פיבונאצ'י:
def fib(n: int) -> int:
    a, b = 1, 1
    for i in range(n):
        a, b = b, a + b
    return a
ואז לבקש מהתלמידים לחשב את הסכום של 100 המספרים הראשונים בסידרת פיבונאצ'י. הפיתרון ה"פשוט" הוא למעשה המסובך ביותר:
print(sum(fib(n) for n in range(100)))
בגלל שהוא כולל המון חישובים מיותרים. במצב כזה הרבה יותר יעיל לכתוב פונקציה חדשה (או לכתוב Generator, אבל זה הנושא של השיעור אז בואו לא נקלקל). הבעיה שלנו כמתכנתים היא שמכל מיני סיבות רוב הזמן אנחנו מעדיפים להשתמש באבסטרקציה הקיימת כדי לפתור בעיות, גם אם היא תקולה, ולא לעצור ולבנות אבסטרקציה חדשה אבל גם לא לכתוב את כל הקוד מחדש. בעולם האמיתי הסיכוי לראות משהו שדומה לחישוב האיטי של סכום פיבונאצ'י הוא מאוד גבוה, והוא גדל ככל שעובדים על מוצרים יותר בשלים (כי יש בהם כבר המון אבסטרקציות). אני רואה את הטיעון לשימוש באבסטרקציה לא טובה במערכת קיימת. כשיש משהו שעובד חבל לא להשתמש בו, וגם זה יכול לחסוך לי המון זמן פיתוח ולחסוך המון באגים. נכון זה לא הכי יעיל אבל בואו נשאיר את השיפור הביצועים לשלב האופטימיזציה. אבל כדאי לראות גם את הצד השני - תיקון האבסטרקציה הוא לא רק סיפור של ביצועים אלא גם של נכונות הקוד, ושל כמה מהר אוכל לבנות את הפיצ'ר הבא. ככל שיותר פיצ'רים בנויים על אבסטרקציה קיימת ובעייתית, כך יהיה קשה יותר בהמשך לשנות אותה. בשימוש באבסטרקציה לא נכונה אנחנו פשוט חופרים לעצמנו בור יותר עמוק. בסיטואציה כזאת הכי טוב לתקן את האבסטרקציה הגרועה ולעשות ריפקטור לכל הקוד שהשתמש בה. אם אין זמן עדיף להתחיל מנגנון חדש. גם אם זה ייקח קצת יותר זמן בפיתוח הפיצ'ר הנוכחי, הזמן הזה יחזיר את עצמו ובדרך גם אנחנו נצבור עוד כמה נקודות במסע להפוך למתכנתים טובים יותר.

ToCode
1 420
אתגרים או תירוצים שנה חדשה, פרויקטים חדשים, חלומות חדשים, אבל כשהחלומות האלה מתחילים להיתקל בקשיים יש לנו שתי אסטרטגיות תגובה- 1. אפשר להתייחס לקושי החדש בתור אתגר ולנסות להתקדם למרות הקושי. גם אם דברים לא יקרו כמו שתכננתי בהתחלה, אני עדיין יכול לעשות את הכי טוב שלי. 2. אפשר להתייחס לקושי החדש בתור תירוץ, ולהסביר לעצמי ולעולם למה אי אפשר להגשים את החלום. דוגמה פשוטה היא מתכנתת שכבר כמה שנים לא מספיק מרוצה מהעבודה שלה, לא מרגישה שהיא מתפתחת ותכל'ס כשמסתכלת אחורה לא קרה שום דבר מעניין מקצועית באותן שנים. כאילו כל השנים האלה התאחדו לשנה אחת ארוכה אבל שעברה כהרף עין, בתחושה שאני בקושי יומיים במקום הזה למרות שאני שם כבר כמה שנים. אותה מתכנתת עשויה להחליט לנצל את השנה החדשה ולחפש עבודה חדשה, אבל אז מהר מאוד היא תגלה שהטכנולוגיה רצה קדימה, העולם כבר לא איפה שהיה לפני 3 שנים וכבר אין לה יותר מדי הזדמנויות. הקושי הזה יכול להיות תירוץ - ידעתי שאני לא מספיק טובה, טוב ציפור אחת ביד, בשביל מה לשבור משהו שעובד, מה חשבתי לעצמי וסך הכל יש המון אנשים שחולמים להיות במקום שלי אז אין לי זכות לקטר, וככה אפשר להעביר עוד שנה, שנתיים ואפילו יותר. או שאפשר להתייחס לקושי בתור אתגר - ברור שאני לא מעודכנת טכנולוגית, שלוש שנים אני תקועה באותה עבודה ללא אופק. בשביל לחפש כמו שצריך אצטרך לרדת לחצי משרה ולהשקיע את הזמן שהתפנה בלימודים, וכן ברור שאצטרך להסתדר עם פחות כסף מה שישפיע על היום יום שלי. אני שמחה לעשות את הוויתורים כדי שהשנה הזאת תהיה מיוחדת עבורי. האם זה יצליח? תלוי בהגדרה שלנו ל"יצליח". אי אפשר לדעת מראש מה יהיו התוצאות החיצוניות של המעשים, אבל אפשר להיות בטוחים שכשאנחנו בוחרים להתמודד עם אתגרים וללכת אחרי החלומות למרות הקשיים השנה הזאת תהיה מיוחדת וחשובה.

ToCode
1 420
טכנולוגיה משעממת או חדשה? אין כמו לעבוד עם טכנולוגיה צעירה, חדשה ומבטיחה שכולם מדברים עליה ורק עכשיו יצאה לשוק. אני לא צריך לשכנע אתכם לעבוד עם טכנולוגיות חדשות כי כולם רצים לשם לבד, ואין כמו הכיף בלהיות הראשון שכותב באיזו שפת תכנות שאף אחד עוד לא שמע עליה. אבל, יש גם לא מעט יתרונות לטכנולוגיות וותיקות ולפרויקטים חשובים אולי כדאי לבדוק פעם נוספת. אלה כמה דברים שאפשר לבנות עליהם כשעובדים עם טכנולוגיות משעממות: 1. חיבור טוב לכל מערכת צד שלישי שאפשר לדמיין. 2. תיעוד טוב ברשת ופיתרונות קיימים לכל בעיה. 3. יכולת שיחה עם Chat GPT בלי להפיל אותו להזיות. 4. קל לגייס אנשים (כן עם כל ההייפ, עדיין קל יותר לגייס מתכנתי Java מאשר מתכנתי Rust). 5. לא משנה איזה בעיה יש לכם, מישהו אחר כבר שבר את הראש וכתב תשובה באינטרנט. הרבה פעמים הפיתרון אפילו מובנה כבר בשפה. מצד שני טכנולוגיה משעממת כשמה כן היא - משעממת, ויש לא מעט מצבים וסיבות לבחור בטכנולוגיות חדשות: 1. אפשר לפתח לבד את כל הכלים ולקבל רוח גבית חזקה מהאינטרנט גם לפרויקטים קטנים. 2. פוסטים הופכים ויראליים הרבה יותר מהר (כי מי בכלל הולך לשתף פוסט על Java היום) 3. אם הימרתם נכון תמצאו את עצמכם עוד שנתיים עם יותר ניסיון תעסוקתי מכל האנשים סביבכם בטכנולוגיה שכבר תיכנס למיינסטרים. 4. אין כמו הרגשת הגילוי כשלומדים פיתרון חדש ומעניין לבעיה, ולטכנולוגיות חדשות יש הרבה יותר סיכוי להפתיע אותנו לטובה. ומה אתכם? יש לכם סיפורי הצלחה או כישלון בעקבות בחירה טכנולוגיה? מוזמנים לשתף בתגובות איזה טכנולוגיות בחרתם ואיך זה נגמר.

ToCode
1 420
פיתרון פונקציונאלי ל Advent Of Code יום 8 חלק 1 בפייתון החלק הראשון של יום 8 ב Advent Of Code היה ממש פשוט וכלל כמה טיפים לגבי פונקציית reduce בפייתון. האתגר אנחנו רוכבים על גמל במדבר וצריכים למצוא את היציאה. קיבלנו מפה ורשימת צעדים שצריך לקחת כדי לצאת, אבל לא אמרו לנו כמה פעמים צריך לעשות את המסלול. קלט לדוגמה נראה ככה:
LLR

AAA = (BBB, BBB)
BBB = (AAA, ZZZ)
ZZZ = (ZZZ, ZZZ)
השורה הראשונה מייצגת מסלול ואחריה יש שורה ריקה ואז המפה. המפה מכילה שורות כשכל שורה מורכבת ממזהה של מקום ואז שתי אפשרויות לאן להמשיך ממנו - האות L אומרת שצריך להמשיך שמאלה לאפשרות הראשונה, והאות R אומרת שצריך להמשיך ימינה לאפשרות השניה. המסלול מתחיל תמיד ב AAA וצריך להגיע ל ZZZ. בדוגמה שלנו נתחיל ב AAA, נלך שמאלה ונגיע ל BBB, אחרי זה שוב שמאלה ואנחנו חוזרים ל AAA (כי בשורה של BBB האפשרות הראשונה היא AAA), אחרי זה ימינה שוב ל BBB. בגלל שההוראות נגמרו אבל עדיין לא הגענו לסוף נבצע אותן פעם נוספת נלך שמאלה, שמאלה וימינה ואז נגיע ל ZZZ. סך הכל לקח לנו 6 צעדים להגיע לסוף. פיענוח הקלט בפייתון צעד ראשון בשביל לתרגם את המנגנון לפייתון הוא פיענוח הקלט וזה דווקא פשוט עם ביטוי רגולארי:
def parse(input: typing.TextIO) -> tuple[str, dict[str, tuple[str, str]]]:
    instructions = input.readline().strip()
    map = {}
    input.readline()
    for line in input:
        if m := re.search(r"(\w+) = \((\w+), (\w+)\)", line):
            key, left, right = m.groups()
            map[key] = (left, right)

    return instructions, map
פיתרון פונקציונאלי עם reduce החלק השני והיותר מעניין הוא חישוב המסלול. את שורת ההוראות אפשר לשים בתוך itertools.cycle כדי לקבל את אפקט החזרה האינסופית ו itertools.takewhile יעזור לנו לרוץ על האיטרטור עד שמגיעים לסוף המסלול, לכן עיקר הקוד הוא פעולת הטיול. פעולת התנועה במבוך היא בדיוק פעולת "קיפול" על רשימת הצעדים, כל פעם לוקחים את המיקום (זה ה Accumulator) ואת הכיוון הנוכחי שמאלה או ימינה, ומתקדמים לפי המפה בכיוון שקיבלנו. לכן הפונקציה היא:
def step(map):
    def handler(location, direction):
        if direction == "L":
            return map[location][0]
        elif direction == "R":
            return map[location][1]
        else:
            raise Exception(f"Unknown direction {direction}")

    return handler
שימו לב למבנה של פונקציה שמחזירה פונקציה, בשביל החתימה שתתאים לפעולת הקיפול. לפייתון יש אומנם פונקציה functools.reduce שמבצעת reduce אבל היא לא מתאימה פה בגלל שהיא לא יודעת לעבוד עם איטרטורים. במקומה נשתמש ב itertools.accumulate כדי לקבל את התוצאה:
instructions, map = parse(open('input.txt'))
print(len(list(itertools.takewhile(lambda n: n != 'ZZZ',
                               itertools.accumulate(itertools.cycle(instructions),
                                                    step(map),
                                                    initial='AAA')))))

ToCode
1 420
שיתוף מידע בין דקורטורים לקוד חיצוני בפייתון הפוסט היום הוא יותר שאלה מאשר טיפ. יש לי כיוון איך לפתור את הבעיה אבל אשמח לשמוע דעות נוספות כי אני לא מרוצה ממנו, והסיפור מתחיל בדקורטורים ושיתוף מידע ביניהם לבין קוד של התוכנית הראשי. תיאור הבעיה נכתוב דקורטור בשם counting שמוסיף לפונקציה מונה שמראה כמה פעמים קראו לה:
def counting(f):
    def inner(x: int) -> int:
        inner.call_count += 1
        result = f(x)
        return result

    inner.call_count = 0
    return inner
אפשר להשתמש בדקורטור באופן הבא:
@counting
def twice(x: int) -> int:
    return x * 2
    
twice(10)
twice(20)
twice(30)

print(f"twice was called {twice.call_count} times")
עכשיו ננסה להשתמש בדקורטור בצורה קצת יותר יצירתית ונראה איך הוא נשבר. התוכנית היא:
@lru_cache()
@counting
def twice(x):
    print(f"twice::{x}")
    return x * 2


twice(10)
twice(10)
twice(10)
twice(10)

print(twice.call_count)
והתוצאה היא הדפסת הערך 0. למה זה שבור הבעיה כמובן היא הדקורטור הנוסף lru_cache. דקורטור זה משנה את הפונקציה twice כך שאני בעצם מפעיל את הפונקציה ש lru_cache מחזיר, בעוד שהערך של call_count נשמר בפונקציה שהדקורטור counting יצר. למעשה בשורה האחרונה של הקוד הבעיה היא שאין לי איך להגיע לאותו ערך שסופר כמה פעמים קראו לפונקציה. מה אפשר לעשות במקום הבעיה עם דקורטורים היא שאין להם באמת איך להחזיר ערך: דקורטור עצמו לא מחזיר ערך, והפונקציה שהוא מחזיר עלולה להידרס בהמשך על ידי דקורטור אחר ולכן גם בה אי אפשר להשתמש כדי לשמור ערכים. המוצא היחיד מהסיפור הזה הוא לשמור את הערך בקוד החיצוני ולהעביר אותו פנימה לתוך הדקורטור, לכן התיקון הזה עובד:
def counting_v2(counter):
    def decorator(f):
        def inner(x: int) -> int:
            print(inner)
            counter.inc()
            # Before calling the decorated function ...
            result = f(x)
            # After calling the decorated function ...
            # Modify the result value
            return result

        return inner
    return decorator

twice_counter = Counter()
@lru_cache()
@counting_v2(twice_counter)
def twice(x):
    print(f"twice::{x}")
    return x * 2


twice(10)
twice(10)
twice(10)
twice(10)

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

ToCode
1 420
אין לי מושג למה כתבתי את זה בקריאת קוד, המחשבה "אין לי מושג למה כתבתי את זה ככה" יכולה להיות מדכאת. היא יכולה לרמוז שחלקים במערכת עובדים בגלל צירוף מקרים גלקטי, שהקוד שלי הוא לא באמת שלי ואולי אין לי את הכישורים והיכולות שצריך בשביל העבודה הזאת. או יותר גרוע, לייצר מעין שכנוע עצמי שכולם פשוט מדביקים כל היום קוד ש AI מייצר בלי להבין אותו ולכן אין סיבה להתאמץ. במקום ליפול לבור הזה, בואו נשתמש ב "אין לי מושג למה כתבתי את זה" בתור דלת ששווה לעבור דרכה כדי ללמוד יותר על איך מערכות עובדות. איך לי מושג למה כתבתי את זה, ולכן אנסה- 1. למחוק את הפונקציה ולשכתב אותה מחדש בדרך הגיונית עבורי. 2. לבקש מחבר או מ AI להסביר לי את המנגנון, ואז להציע עוד כמה דרכים לכתוב את אותו דבר. 3. לכתוב מספיק בדיקות כדי לשבור את הקוד, ולהבין איך הוא נשבר ומתי הוא עובד. נ.ב. הנה גם רעיון למוצר AI קטן - תוסף ל IDE שמזהה ומסמן קטעי קוד שנכתבו בלי הבנה מספיק טובה. אני בטוח הייתי שמח לנסות אותו על כמה פרויקטים שלי.

ToCode
1 420
1. הכי הרבה זמן שרפתי על המימוש של פונקציית המיפוי Day5Map::getDestinations, בעיקר בגלל שניסיתי למצוא לולאה מובנית בסקאלה שתעזור לי במימוש הזה. אחרי שהתעייפתי מהחיפוש בניתי לבד את הפונקציה הרקורסיבית ומשם הכל רץ מהר. 2. אני עדיין לא בטוח שאני משתמש נכון ב Case Classes של סקאלה. מצד אחד הם מחזיקים data בצורה מאוד נוחה אבל מצד שני מהר מאוד זה מרגיש כמו פיתוח מונחה עצמים. אולי המפתח שצריך לזכור כאן הוא שדברים הם Immutable וכל פעם שרוצים לשנות משהו "של האוביקט" בתוך פונקציה מחזירים אוביקט חדש במקום. 3. מנגנון Pattern Matching של סקאלה עובד טוב כולל בצורה מקוננת. קוד כזה נראה ממש הגיוני לכתיבה וקריאה:
      sourceIntervals match
        case Nil => (sourceIntervals, destinationIntervals)
        case head :: tail =>
          this.ranges.find { r => ! r.getDestination(head).isEmpty } match
            case None => loop(tail, destinationIntervals :+ head)
            case Some(r) => loop(tail ++ r.getLeftover(head), destinationIntervals :+ r.getDestination(head))
מכירים סקאלה ויש לכם רעיונות איך לשפר את הקוד? אל תתביישו לספר לי בתגובות.

ToCode
1 420
start = Math.max(source.start, this.sourceStart) + offset,
      end = Math.min(source.end, sourceStart + size) + offset,
      step = 1)

  def getLeftover(source: Interval): List[Interval] =
    source.without(NumericRange.Exclusive[Long](
      start = this.sourceStart,
      end = this.sourceStart + this.size,
      step = 1
    ))
}
המחלקה שומרת את שלושת המספרים וכוללת שתי פונקציות למיפוי טווחים, אחת מחזירה את טווח היעד אם טווח המקור מתאים לכלל המיפוי, והשניה תחזיר את החלק בטווח המקור שלא התאים לכלל המיפוי, במקרה שהיה צריך לשבור את הטווח לכמה חלקים. המחלקה השניה מייצגת מפה שלמה, כלומר אוסף של כללי מיפוי. אני שמרתי גם את השם שלה למרות שהוא לא חשוב לפיתרון:
case class Day5Map(name: String, ranges: List[Day5Range]) {
  def getDestinations(sources: List[Interval]): List[Interval] =
    @tailrec
    def loop(sourceIntervals: List[Interval], destinationIntervals: List[Interval]): (List[Interval], List[Interval]) =
      sourceIntervals match
        case Nil => (sourceIntervals, destinationIntervals)
        case head :: tail =>
          this.ranges.find { r => ! r.getDestination(head).isEmpty } match
            case None => loop(tail, destinationIntervals :+ head)
            case Some(r) => loop(tail ++ r.getLeftover(head), destinationIntervals :+ r.getDestination(head))

    loop(sources, List())._2

  def withAddedRange(range: Day5Range): Day5Map = {
    Day5Map(this.name, this.ranges :+ range)
  }
}
הפונקציה המעניינת כאן היא getDestinations, שלוקחת רשימה של טווחים ומחזירה רשימה חדשה של טווחים שהם בעצם התוצאה של הפעלת כללי המיפוי של אותה מפה. בשביל זה לוקחים כל טווח מרשימת המקורות, רצים על כללי המיפוי ומחפשים כלל שמתאים לו. אם מצאנו נשבור את הטווח לכמה חלקים, נמפה את החלק שמתאים לכלל המיפוי ואת החלקים שלא התאימו משאירים ברשימת המקורות לאיטרציה הבאה. את פיענוח הקלט עשיתי בשלבים בגלל סיבוך מהחלק הראשון. המחלקה ששומרת את הקלט נראית כך:
case class Day5Part2Input(seeds: List[Interval], maps: List[Day5Map])

case class Day5Input(seeds: List[Long], maps: List[Day5Map]) {
  def toPart2Input: Day5Part2Input =
    Day5Part2Input(
      seeds = seeds.grouped(2).toList.map {
        case List(a, b) => a.until(a + b)
      },
      maps = maps
    )
}
והקוד שבונה אותה הוא:
  def parseInput(source: Source): Day5Input =
    val newMapPattern: Regex = """([-\w]+) map:""".r
    val seedsPattern: Regex = """seeds: ([\d\s]+)""".r
    val mapLine: Regex = """(\d+) (\d+) (\d+)""".r

    source
      .getLines()
      .foldLeft(Day5Input(List(), List()))((input, line) => {
        line match
          case seedsPattern(seeds) => Day5Input(seeds.split(' ').map(_.toLong).toList, List())
          case newMapPattern(mapName) => Day5Input(
            input.seeds,
            input.maps :+ Day5Map(mapName, List())
          )
          case mapLine(destinationStart, sourceStart, size) =>
            val range = Day5Range(sourceStart.toLong, destinationStart.toLong, size.toLong)
            Day5Input(
              input.seeds,
              input.maps.updated(input.maps.size - 1, input.maps.last.withAddedRange(range))
            )
          case _ => input
      })
הלולאה המרכזית בקוד הזה היא לולאת reduce שעוברת שורה שורה ומעדכנת את אוביקט הקלט שאנחנו בונים לפי התוכן של השורה. זאת תבנית שאני משתמש בה הרבה בעבודה על קלט טקסטואלי וסך הכל הקוד די מספר בצורה ברורה מה עושים עם כל סוג שורה. הפונקציה האחרונה בתוכנית רצה על הקלט, מבצעת את כל המיפויים ומחפשת את התוצאה הקטנה ביותר והיא נראית כך:
  @main
  def day5_part2(): Unit = {
    val input = parseInput(Source.fromResource("day5.txt")).toPart2Input

    val destinations = input.maps.foldLeft(input.seeds)((sources, map) => map.getDestinations(sources))
    println(destinations.minBy(i => i.start).start)
  }
כמה לקחים על סקאלה ומחשבות כלליות על הפיתרון:

ToCode
1 420
פיתרון Advent Of Code יום 5 חלק 2 בסקאלה את יום 5 של Advent Of Code השנה לקח לי הרבה זמן לעבד, למרות שבסופו של דבר הפיתרון לא היה יותר מדי מסובך. כמו תמיד בסידרה הזאת נתחיל עם תיאור התרגיל ואז פיתרון בשפת סקאלה, עם כמה מילים על סקאלה עצמה. מה צריך למצוא הקלט היום היה ארוך ומסורבל במיוחד, וזאת היתה רק ההתחלה. אנחנו עם קלט דוגמה שנראה ככה:
seeds: 79 14 55 13

seed-to-soil map:
50 98 2
52 50 48

soil-to-fertilizer map:
0 15 37
37 52 2
39 0 15

fertilizer-to-water map:
49 53 8
0 11 42
42 0 7
57 7 4

water-to-light map:
88 18 7
18 25 70

light-to-temperature map:
45 77 23
81 45 19
68 64 13

temperature-to-humidity map:
0 69 1
1 0 69

humidity-to-location map:
60 56 37
56 93 4
המשמעות שלו קצת משתנה בין החלקים, אבל אנחנו נתמקד כאן בחלק השני. אז שימו לב שאנחנו מתחילים עם שורה של ערכים עם התחילית seeds, ואחריה אוסף של בלוקים של "מפות". הערכים הם בעצם טווחים של ערכים, ואנחנו צריכים לזהות איך הטווחים האלה הולכים להשתנות באמצעות המפות בהמשך הטקסט. טווח מצוין על ידי המספר בו הוא מתחיל ואז גודל הטווח, אז שורת ה seeds בדוגמה מדברת על שני טווחים, הראשון מתחיל ב 79 וכולל 14 מספרים כלומר עד המספר 92 ועד בכלל, והשני מתחיל במספר 55 וגודלו 13 מספרים. את המפות יש לקרוא בתור אוסף של כללי מיפוי, כל כלל מורכב מ-3 מספרים, המספר האמצעי אומר מאיזה מספר הטווח מתחיל, השלישי אומר מה אורך הטווח והמספר הראשון מציין את ההתחלה של טווח היעד, כך בטקסט הדוגמה המיפוי:
50 98 2
אומר שהמספר 98 ימופה למספר 50, והמספר 99 ימופה למספר 51 - או במילים אחרות, כל הטווח שמתחיל ב 98 ואורכו 2 מספרים ימופה לטווח באורך שני מספרים שמתחיל ב 50. עכשיו המשימה - יש לקחת את הטווחים משורת ה seeds, למפות אותם דרך המפות עד שמגיעים למפה האחרונה ולהדפיס מה המספר הקטן ביותר שאפשר להגיע אליו. בנוסף מספר שאין לו מיפוי במפה מסוימת נשאר כמו שהוא ויוכל להתמפות במפה הבאה. איך לגשת לזה מה שמבלבל בתרגיל הזה הוא שטווח לא ממופה "כמו שהוא" בין מפה אחת לשניה, לדוגמה אם ניקח את כלל המיפוי:
52 50 48
ואת הטווח מ 60 עד 70 אז החיים קלים כי כל הטווח נמצא בתוך כלל המיפוי, ואז 60 ימופה ל 62, 61 ימופה ל 63 ו 69 ימופה ל 71, סך הכל הטווח מ 60 עד 70 אחרי יישום כלל המיפוי יהפוך לטווח 62 עד 72. אבל אם הטווח שהתחלתי איתו הוא מ 40 עד 60 אז המצב יותר מסובך, כי המספרים מ 50 עד 60 ימופו לפי כלל המיפוי ויהפכו ל 52 עד 62, אבל המספרים מ 40 עד 50 יישארו כמו שהם כי כלל המיפוי לא מתאים להם, וכך מטווח מקור אחד קיבלתי מיפוי לשני טווחים במפה הבאה. לכן לב הפיתרון יהיה פונקציה רקורסיבית שתיקח רשימה של טווחים ורשימה של כללי מיפוי, ותעביר את כל הטווחים דרך כללי המיפוי עד לקבלת רשימה חדשה של טווחים, אותה נשלח לכללי המיפוי בבלוק הבא. פיתרון בסקאלה אני מתחיל בתוספת ל Range ומוסיף פונקציה ששוברת אותו לפי Range אחר ומחזירה את החלקים מה Range שלא נמצאים ב Range השני. זה יעזור לנו להתמודד עם טווחים שנמצאים רק חלקית בתוך כלל מיפוי:
import scala.annotation.tailrec
import scala.collection.immutable.NumericRange
import scala.io.Source
import scala.util.chaining.*
import scala.util.matching.Regex

type Interval = NumericRange.Exclusive[Long]

extension (self: Interval) {
  def without(other: Interval): List[Interval] =
    if (self.isEmpty || other.isEmpty || self.end <= other.start || self.start >= other.end) {
      List(self)
    } else {
      val left = if (self.start < other.start) List(NumericRange.Exclusive[Long](self.start, other.start, step = 1)) else Nil
      val right = if (self.end > other.end) List(NumericRange.Exclusive[Long](other.end, self.end, step = 1)) else Nil
      left ++ right
    }
}

מכאן אפשר להמשיך למחלקה שמייצגת כלל מיפוי. בגלל שבכל מפה יש הרבה כללי מיפוי ולקח לי זמן להבין את התרגיל ואת התפקיד של כל חלק הלכתי על שם קצת טפשי למחלקה זו:
case class Day5Range(sourceStart: Long, destinationStart: Long, size: Long) {
  def toSourceRange: Interval =
    this.sourceStart.until(this.size + this.sourceStart)

  def getDestination(source: Interval): Interval =
    val offset = this.destinationStart - this.sourceStart
    NumericRange.Exclusive[Long](

ToCode
1 420
מכורים להפסדים אם יש דבר שהמוח האנושי שונא יותר מכל זה להודות בהפסד, מה שגורם בצורה אבסורדית דווקא ליותר הפסדים או הפסדים יותר גדולים. כמו מהמר שלא מוכן להודות בהפסד ורק מעלה את סכום ההימור, כך מתכנתים עם כוונות טובות מצליחים להמשיך לעבוד על פיצ'ר שהוא בעצם חור שחור, בלי להבין שהקוד שכבר כתבנו הוא המלכודת ושום דבר טוב לא יצא מהסבך הזה. יש ימים שהדבר הכי טוב שאפשר לעשות בשביל הפרויקט הוא git stash. כן סטאש, לא ליצור איזה בראנץ שעוד חודשיים מישהו יבוא ויציע "להחיות את הפיצ'ר", סטאש שלא רואים (האמיצות יכולות לנסות git restore, אבל לי אף פעם לא היה אומץ), ושלא מספרים עליו לאף אחד. לקבור את הפיצ'ר, להוציא את הגירסה ולהמשיך הלאה. בספרינט הבא אולי נבנה את זה מחדש מאפס בדרך אחרת.

ToCode
1 420
היום למדתי: עדיף להשוות טיפוסים בפייתון עם is הלינטר של פייתון התרגז עליי היום כי כתבתי קוד כזה:
if type(x) == int:
    ...
ולא, לא עניין אותו אם אני משתמש ב type או ב isinstance הפעם ובכל מקרה באמת רציתי לבדוק את ה type. מה שהפריע לו היה דווקא ה ==, כי בעולם של הלינטר השוואה בין טיפוסים תעבוד טוב יותר עם is, כלומר זה הקוד שהייתי צריך לכתוב:
if type(x) is int:
    ...
כי בעבודה עם טיפוסים זהות ושיוויון זה אותו דבר. נו, הלכתי לבדוק אם יש הבדל בזמן ריצה בין שתי האפשרויות:
In [2]: %timeit type(8) == int
15.6 ns ± 0.091 ns per loop (mean ± std. dev. of 7 runs, 100,000,000 loops each)

In [3]: %timeit type(8) is int
13.4 ns ± 0.0661 ns per loop (mean ± std. dev. of 7 runs, 100,000,000 loops each)
וכן יש, אבל כל כך קטן שזה לא באמת משנה, מה שהופך את השאלה לעניין של טעם. ומאחר ועל טעם ועל ריח לא מתווכחים עם PEP8 אנחנו נשארים עם is כדי להשוות טיפוסים, כמו גם השוואות ל None. נ.ב. במקומות יותר מעניינים השוואה עם is או == יכולה להיות בעלת משמעות, למשל בעבודה עם רשימות:
In [14]: [1, 2, 3] == [1, 2, 3]
Out[14]: True

In [15]: [1, 2, 3] is [1, 2, 3]
Out[15]: False
אבל במקרים כאלה בדרך כלל נשקיע יותר מחשבה בבחירת האופרטור הנכון שיתאים למה שהתוכנית אמורה לעשות.